Merge branch 'main.compilation-adjustments' into main

Signed-off-by: David Bureš <buresdv@gmail.com>
This commit is contained in:
David Bureš 2025-03-29 18:01:29 +01:00 committed by GitHub
commit 93386c2385
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
139 changed files with 10874 additions and 2500 deletions

20
.github/workflows/tuist-test.yml vendored Normal file
View File

@ -0,0 +1,20 @@
name: Test Application
on:
pull_request:
branches:
- main
push:
branches:
- main
jobs:
test:
runs-on: macos-15
steps:
- uses: actions/checkout@v3
- uses: jdx/mise-action@v2
- name: Select Xcode
run: sudo xcode-select -switch /Applications/Xcode_16.1.app
- name: Fix plugins
run: defaults write com.apple.dt.Xcode IDESkipPackagePluginFingerprintValidatation -bool YES
- run: tuist install
- run: tuist test Cork

View File

@ -78,7 +78,16 @@ identifier_name:
- GlobalAPIKey
allowed_symbols: ["_"]
reporter: "xcode" # reporter type (xcode, json, csv, checkstyle, codeclimate, junit, html, emoji, sonarqube, markdown, github-actions-logging, summary)
#custom_rules:
custom_rules:
enum_associated_value_let_syntax:
name: "Enum Associated Value Let Syntax"
regex: 'case\s+\.?\w+\([^)]*:\s*[^)]'
message: "Use `let` syntax for enum associated values (e.g., `case .example(let value)` instead of `case .example(value: _)`)"
severity: warning
match_kinds:
- keyword # matches 'case'
- identifier # matches enum case names
- attribute.builtin # matches associated value declarations
# brackets_newline:
# name: "Opening Brackets not on Next Line"
# message: "Opening brackets should be placed on their own lines"

View File

@ -18,7 +18,6 @@ class AppState: ObservableObject
// MARK: - Licensing
@Published var licensingState: LicensingState = .notBoughtOrHasNotActivatedDemo
@Published var isShowingLicensingSheet: Bool = false
// MARK: - Navigation
@ -29,24 +28,12 @@ class AppState: ObservableObject
@Published var notificationEnabledInSystemSettings: Bool?
@Published var notificationAuthStatus: UNAuthorizationStatus = .notDetermined
// MARK: - Stuff for controlling various sheets from the menu bar
@Published var isShowingInstallationSheet: Bool = false
@Published var isShowingPackageReinstallationSheet: Bool = false
@Published var isShowingUninstallationSheet: Bool = false
@Published var isShowingMaintenanceSheet: Bool = false
@Published var isShowingFastCacheDeletionMaintenanceView: Bool = false
@Published var isShowingAddTapSheet: Bool = false
@Published var isShowingUpdateSheet: Bool = false
// MARK: - Stuff for controlling the UI in general
@Published var isSearchFieldFocused: Bool = false
// MARK: - Brewfile importing and exporting
@Published var isShowingBrewfileExportProgress: Bool = false
@Published var isShowingBrewfileImportProgress: Bool = false
@Published var brewfileImportingStage: BrewfileImportStage = .importing
@Published var isCheckingForPackageUpdates: Bool = true
@ -54,28 +41,37 @@ class AppState: ObservableObject
@Published var isShowingUninstallationProgressView: Bool = false
@Published var isShowingFatalError: Bool = false
@Published var fatalAlertType: DisplayableAlert? = nil
@Published var sheetToShow: DisplayableSheet? = nil
@Published var isShowingSudoRequiredForUninstallSheet: Bool = false
@Published var packageTryingToBeUninstalledWithSudo: BrewPackage?
@Published var isShowingRemoveTapFailedAlert: Bool = false
@Published var isShowingIncrementalUpdateSheet: Bool = false
// MARK: - Loading of packages and taps
@Published var isLoadingFormulae: Bool = true
@Published var isLoadingCasks: Bool = true
@Published var isLoadingTaps: Bool = true
@Published var isLoadingTopPackages: Bool = false
// MARK: - Loading errors
@Published var failedWhileLoadingFormulae: Bool = false
@Published var failedWhileLoadingCasks: Bool = false
@Published var failedWhileLoadingTaps: Bool = false
@Published var failedWhileLoadingTopPackages: Bool = false
@Published var cachedDownloadsFolderSize: Int64 = AppConstants.shared.brewCachedDownloadsPath.directorySize
@Published var cachedDownloads: [CachedDownload] = .init()
private var cachedDownloadsTemp: [CachedDownload] = .init()
// MARK: - Tagging
@Published var taggedPackageNames: Set<String> = .init()
@Published var corruptedPackage: String = ""
// MARK: - Other
var enableExtraAnimations: Bool
{
return UserDefaults.standard.bool(forKey: "enableExtraAnimations")
}
// MARK: - Showing errors
@ -92,6 +88,18 @@ class AppState: ObservableObject
fatalAlertType = nil
}
// MARK: - Showing sheets
func showSheet(ofType sheetType: DisplayableSheet)
{
self.sheetToShow = sheetType
}
func dismissSheet()
{
self.sheetToShow = nil
}
// MARK: - Notification setup
@ -149,74 +157,10 @@ class AppState: ObservableObject
@objc func startUpdateProcessForLegacySelectors(_: NSMenuItem!)
{
isShowingUpdateSheet = true
self.showSheet(ofType: .fullUpdate)
sendNotification(title: String(localized: "notification.upgrade-process-started"))
}
func loadCachedDownloadedPackages() async
{
let smallestDispalyableSize: Int = .init(cachedDownloadsFolderSize / 50)
var packagesThatAreTooSmallToDisplaySize: Int = 0
guard let cachedDownloadsFolderContents: [URL] = try? FileManager.default.contentsOfDirectory(at: AppConstants.shared.brewCachedDownloadsPath, includingPropertiesForKeys: [.isRegularFileKey], options: [.skipsHiddenFiles])
else
{
return
}
let usableCachedDownloads: [URL] = cachedDownloadsFolderContents.filter { $0.pathExtension != "json" }
for usableCachedDownload in usableCachedDownloads
{
guard var itemName: String = try? usableCachedDownload.lastPathComponent.regexMatch("(?<=--)(.*?)(?=\\.)")
else
{
return
}
AppConstants.shared.logger.debug("Temp item name: \(itemName, privacy: .public)")
if itemName.contains("--")
{
do
{
itemName = try itemName.regexMatch(".*?(?=--)")
}
catch {}
}
guard let itemAttributes = try? FileManager.default.attributesOfItem(atPath: usableCachedDownload.path)
else
{
return
}
guard let itemSize = itemAttributes[.size] as? Int
else
{
return
}
if itemSize < smallestDispalyableSize
{
packagesThatAreTooSmallToDisplaySize = packagesThatAreTooSmallToDisplaySize + itemSize
}
else
{
cachedDownloads.append(CachedDownload(packageName: itemName, sizeInBytes: itemSize))
}
AppConstants.shared.logger.debug("Others size: \(packagesThatAreTooSmallToDisplaySize, privacy: .public)")
}
AppConstants.shared.logger.log("Cached downloads contents: \(self.cachedDownloads)")
cachedDownloads = cachedDownloads.sorted(by: { $0.sizeInBytes < $1.sizeInBytes })
cachedDownloads.append(.init(packageName: String(localized: "start-page.cached-downloads.graph.other-smaller-packages"), sizeInBytes: packagesThatAreTooSmallToDisplaySize, packageType: .other))
}
}
private extension UNUserNotificationCenter
@ -226,36 +170,3 @@ private extension UNUserNotificationCenter
await notificationSettings().authorizationStatus
}
}
extension AppState
{
func assignPackageTypeToCachedDownloads(brewData: BrewDataStorage)
{
var cachedDownloadsTracker: [CachedDownload] = .init()
AppConstants.shared.logger.debug("Package tracker in cached download assignment function has \(brewData.installedFormulae.count + brewData.installedCasks.count) packages")
for cachedDownload in cachedDownloads
{
let normalizedCachedPackageName: String = cachedDownload.packageName.onlyLetters
if brewData.installedFormulae.contains(where: { $0.name.localizedCaseInsensitiveContains(normalizedCachedPackageName) })
{ /// The cached package is a formula
AppConstants.shared.logger.debug("Cached package \(cachedDownload.packageName) (\(normalizedCachedPackageName)) is a formula")
cachedDownloadsTracker.append(.init(packageName: cachedDownload.packageName, sizeInBytes: cachedDownload.sizeInBytes, packageType: .formula))
}
else if brewData.installedCasks.contains(where: { $0.name.localizedCaseInsensitiveContains(normalizedCachedPackageName) })
{ /// The cached package is a cask
AppConstants.shared.logger.debug("Cached package \(cachedDownload.packageName) (\(normalizedCachedPackageName)) is a cask")
cachedDownloadsTracker.append(.init(packageName: cachedDownload.packageName, sizeInBytes: cachedDownload.sizeInBytes, packageType: .cask))
}
else
{ /// The cached package cannot be found
AppConstants.shared.logger.debug("Cached package \(cachedDownload.packageName) (\(normalizedCachedPackageName)) is unknown")
cachedDownloadsTracker.append(.init(packageName: cachedDownload.packageName, sizeInBytes: cachedDownload.sizeInBytes, packageType: .unknown))
}
}
cachedDownloads = cachedDownloadsTracker
}
}

View File

@ -77,20 +77,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject
}
}
func applicationWillTerminate(_: Notification)
{
AppConstants.shared.logger.debug("Will die...")
do
{
try saveTaggedIDsToDisk(appState: appState)
}
catch let dataSavingError as NSError
{
AppConstants.shared.logger.error("Failed while trying to save data to disk: \(dataSavingError, privacy: .public)")
}
AppConstants.shared.logger.debug("Died")
}
func applicationDockMenu(_: NSApplication) -> NSMenu?
{
let menu: NSMenu = .init()
@ -105,7 +91,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject
updatePackagesMenuItem.title = String(localized: "start-page.updates.loading")
updatePackagesMenuItem.isEnabled = false
}
else if appState.isShowingUpdateSheet
else if appState.sheetToShow == .fullUpdate
{
updatePackagesMenuItem.title = String(localized: "update-packages.updating.updating")
updatePackagesMenuItem.isEnabled = false

View File

@ -0,0 +1,12 @@
{
"info" : {
"author" : "xcode",
"version" : 1
},
"symbols" : [
{
"filename" : "custom.macwindow.badge.xmark.svg",
"idiom" : "universal"
}
]
}

View File

@ -0,0 +1,137 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--Generator: Apple Native CoreSVG 341-->
<!DOCTYPE svg
PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 3300 2200">
<!--glyph: "", point size: 100.0, font version: "20.0d10e1", template writer version: "138.0.0"-->
<style>.monochrome-0 {-sfsymbols-motion-group:1;-sfsymbols-layer-tags:-2de6013cdb0de8d2 -77f23b8a0c0c9a0a macwindow}
.monochrome-1 {opacity:0.0;-sfsymbols-clear-behind:true;-sfsymbols-motion-group:1;-sfsymbols-layer-tags:-2de6013cdb0de8d2 -77f23b8a0c0c9a0a macwindow}
.monochrome-2 {opacity:0.0;-sfsymbols-clear-behind:true;-sfsymbols-motion-group:1;-sfsymbols-layer-tags:-2de6013cdb0de8d2 -77f23b8a0c0c9a0a macwindow}
.monochrome-3 {opacity:0.0;-sfsymbols-clear-behind:true;-sfsymbols-motion-group:1;-sfsymbols-layer-tags:-2de6013cdb0de8d2 -77f23b8a0c0c9a0a macwindow}
.monochrome-4 {opacity:0.0;-sfsymbols-clear-behind:true;-sfsymbols-motion-group:0;-sfsymbols-layer-tags:-2de6013cdb0de8d2 _badge xmark}
.monochrome-5 {-sfsymbols-motion-group:0;-sfsymbols-always-pulses:true;-sfsymbols-layer-tags:-2de6013cdb0de8d2 _badge xmark}
.monochrome-6 {opacity:0.0;-sfsymbols-clear-behind:true;-sfsymbols-motion-group:0;-sfsymbols-always-pulses:true;-sfsymbols-layer-tags:-2de6013cdb0de8d2 _badge xmark}
.multicolor-0:tintColor {-sfsymbols-motion-group:1;-sfsymbols-layer-tags:-2de6013cdb0de8d2 -77f23b8a0c0c9a0a macwindow}
.multicolor-1:tintColor {opacity:0.0;-sfsymbols-clear-behind:true;-sfsymbols-motion-group:1;-sfsymbols-layer-tags:-2de6013cdb0de8d2 -77f23b8a0c0c9a0a macwindow}
.multicolor-2:tintColor {opacity:0.0;-sfsymbols-clear-behind:true;-sfsymbols-motion-group:1;-sfsymbols-layer-tags:-2de6013cdb0de8d2 -77f23b8a0c0c9a0a macwindow}
.multicolor-3:tintColor {opacity:0.0;-sfsymbols-clear-behind:true;-sfsymbols-motion-group:1;-sfsymbols-layer-tags:-2de6013cdb0de8d2 -77f23b8a0c0c9a0a macwindow}
.multicolor-4:tintColor {opacity:0.0;-sfsymbols-clear-behind:true;-sfsymbols-motion-group:0;-sfsymbols-layer-tags:-2de6013cdb0de8d2 _badge xmark}
.multicolor-5:systemRedColor {-sfsymbols-motion-group:0;-sfsymbols-always-pulses:true;-sfsymbols-layer-tags:-2de6013cdb0de8d2 _badge xmark}
.multicolor-6:white {-sfsymbols-motion-group:0;-sfsymbols-always-pulses:true;-sfsymbols-layer-tags:-2de6013cdb0de8d2 _badge xmark}
.hierarchical-0:secondary {-sfsymbols-motion-group:1;-sfsymbols-layer-tags:-2de6013cdb0de8d2 -77f23b8a0c0c9a0a macwindow}
.hierarchical-1:primary {opacity:0.0;-sfsymbols-clear-behind:true;-sfsymbols-motion-group:1;-sfsymbols-layer-tags:-2de6013cdb0de8d2 -77f23b8a0c0c9a0a macwindow}
.hierarchical-2:primary {opacity:0.0;-sfsymbols-clear-behind:true;-sfsymbols-motion-group:1;-sfsymbols-layer-tags:-2de6013cdb0de8d2 -77f23b8a0c0c9a0a macwindow}
.hierarchical-3:primary {opacity:0.0;-sfsymbols-clear-behind:true;-sfsymbols-motion-group:1;-sfsymbols-layer-tags:-2de6013cdb0de8d2 -77f23b8a0c0c9a0a macwindow}
.hierarchical-4:primary {opacity:0.0;-sfsymbols-clear-behind:true;-sfsymbols-motion-group:0;-sfsymbols-layer-tags:-2de6013cdb0de8d2 _badge xmark}
.hierarchical-5:primary {-sfsymbols-motion-group:0;-sfsymbols-layer-tags:-2de6013cdb0de8d2 _badge xmark}
.hierarchical-6:primary {opacity:0.0;-sfsymbols-clear-behind:true;-sfsymbols-motion-group:0;-sfsymbols-layer-tags:-2de6013cdb0de8d2 _badge xmark}
.SFSymbolsPreviewWireframe {fill:none;opacity:1.0;stroke:black;stroke-width:0.5}
</style>
<g id="Notes">
<rect height="2200" id="artboard" style="fill:white;opacity:1" width="3300" x="0" y="0"/>
<line style="fill:none;stroke:black;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="292" y2="292"/>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;font-weight:bold;" transform="matrix(1 0 0 1 263 322)">Weight/Scale Variations</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 559.711 322)">Ultralight</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 856.422 322)">Thin</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 1153.13 322)">Light</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 1449.84 322)">Regular</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 1746.56 322)">Medium</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 2043.27 322)">Semibold</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 2339.98 322)">Bold</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 2636.69 322)">Heavy</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 2933.4 322)">Black</text>
<line style="fill:none;stroke:black;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="1903" y2="1903"/>
<g transform="matrix(0.2 0 0 0.2 263 1933)">
<path d="m46.2402 4.15039c21.7773 0 39.4531-17.627 39.4531-39.4043s-17.6758-39.4043-39.4531-39.4043c-21.7285 0-39.4043 17.627-39.4043 39.4043s17.6758 39.4043 39.4043 39.4043Zm0-7.42188c-17.6758 0-31.9336-14.3066-31.9336-31.9824s14.2578-31.9824 31.9336-31.9824 31.9824 14.3066 31.9824 31.9824-14.3066 31.9824-31.9824 31.9824Zm-17.9688-31.9824c0 2.14844 1.51367 3.61328 3.75977 3.61328h10.498v10.5957c0 2.19727 1.46484 3.71094 3.61328 3.71094 2.24609 0 3.71094-1.51367 3.71094-3.71094v-10.5957h10.5957c2.19727 0 3.71094-1.46484 3.71094-3.61328 0-2.19727-1.51367-3.71094-3.71094-3.71094h-10.5957v-10.5469c0-2.24609-1.46484-3.75977-3.71094-3.75977-2.14844 0-3.61328 1.51367-3.61328 3.75977v10.5469h-10.498c-2.24609 0-3.75977 1.51367-3.75977 3.71094Z"/>
</g>
<g transform="matrix(0.2 0 0 0.2 281.506 1933)">
<path d="m58.5449 14.5508c27.4902 0 49.8047-22.3145 49.8047-49.8047s-22.3145-49.8047-49.8047-49.8047-49.8047 22.3145-49.8047 49.8047 22.3145 49.8047 49.8047 49.8047Zm0-8.30078c-22.9492 0-41.5039-18.5547-41.5039-41.5039s18.5547-41.5039 41.5039-41.5039 41.5039 18.5547 41.5039 41.5039-18.5547 41.5039-41.5039 41.5039Zm-22.6562-41.5039c0 2.39258 1.66016 4.00391 4.15039 4.00391h14.3555v14.4043c0 2.44141 1.66016 4.15039 4.05273 4.15039 2.44141 0 4.15039-1.66016 4.15039-4.15039v-14.4043h14.4043c2.44141 0 4.15039-1.61133 4.15039-4.00391 0-2.44141-1.70898-4.15039-4.15039-4.15039h-14.4043v-14.3555c0-2.49023-1.70898-4.19922-4.15039-4.19922-2.39258 0-4.05273 1.70898-4.05273 4.19922v14.3555h-14.3555c-2.49023 0-4.15039 1.70898-4.15039 4.15039Z"/>
</g>
<g transform="matrix(0.2 0 0 0.2 304.924 1933)">
<path d="m74.8535 28.3203c35.1074 0 63.623-28.4668 63.623-63.5742s-28.5156-63.623-63.623-63.623-63.5742 28.5156-63.5742 63.623 28.4668 63.5742 63.5742 63.5742Zm0-9.08203c-30.127 0-54.4922-24.3652-54.4922-54.4922s24.3652-54.4922 54.4922-54.4922 54.4922 24.3652 54.4922 54.4922-24.3652 54.4922-54.4922 54.4922Zm-28.8574-54.4922c0 2.58789 1.85547 4.39453 4.58984 4.39453h19.7266v19.7754c0 2.68555 1.85547 4.58984 4.44336 4.58984 2.68555 0 4.54102-1.85547 4.54102-4.58984v-19.7754h19.7754c2.68555 0 4.58984-1.80664 4.58984-4.39453 0-2.73438-1.85547-4.58984-4.58984-4.58984h-19.7754v-19.7266c0-2.73438-1.85547-4.63867-4.54102-4.63867-2.58789 0-4.44336 1.9043-4.44336 4.63867v19.7266h-19.7266c-2.73438 0-4.58984 1.85547-4.58984 4.58984Z"/>
</g>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;font-weight:bold;" transform="matrix(1 0 0 1 263 1953)">Design Variations</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 1971)">Symbols are supported in up to nine weights and three scales.</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 1989)">For optimal layout with text and other symbols, vertically align</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 2007)">symbols with the adjacent text.</text>
<line style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="776" x2="776" y1="1919" y2="1933"/>
<g transform="matrix(0.2 0 0 0.2 776 1933)">
<path d="m16.5527 0.78125c2.58789 0 3.85742-0.976562 4.78516-3.71094l6.29883-17.2363h28.8086l6.29883 17.2363c0.927734 2.73438 2.19727 3.71094 4.73633 3.71094 2.58789 0 4.24805-1.5625 4.24805-4.00391 0-0.830078-0.146484-1.61133-0.537109-2.63672l-22.9004-60.9863c-1.12305-2.97852-3.125-4.49219-6.25-4.49219-3.02734 0-5.07812 1.46484-6.15234 4.44336l-22.9004 61.084c-0.390625 1.02539-0.537109 1.80664-0.537109 2.63672 0 2.44141 1.5625 3.95508 4.10156 3.95508Zm13.4766-28.3691 11.8652-32.8613h0.244141l11.8652 32.8613Z"/>
</g>
<line style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="792.836" x2="792.836" y1="1919" y2="1933"/>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;font-weight:bold;" transform="matrix(1 0 0 1 776 1953)">Margins</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 776 1971)">Leading and trailing margins on the left and right side of each symbol</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 776 1989)">can be adjusted by modifying the x-location of the margin guidelines.</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 776 2007)">Modifications are automatically applied proportionally to all</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 776 2025)">scales and weights.</text>
<g transform="matrix(0.2 0 0 0.2 1289 1933)">
<path d="m14.209 9.32617 8.49609 8.54492c4.29688 4.3457 9.22852 4.05273 13.8672-1.07422l53.4668-58.9355-4.83398-4.88281-53.0762 58.3984c-1.75781 2.00195-3.41797 2.49023-5.76172 0.146484l-5.85938-5.81055c-2.34375-2.29492-1.80664-4.00391 0.195312-5.81055l57.373-54.0039-4.88281-4.83398-57.959 54.4434c-4.93164 4.58984-5.32227 9.47266-1.02539 13.8184Zm32.0801-90.9668c-2.09961 2.05078-2.24609 4.93164-1.07422 6.88477 1.17188 1.80664 3.4668 2.97852 6.68945 2.14844 7.32422-1.70898 14.9414-2.00195 22.0703 2.68555l-2.92969 7.27539c-1.70898 4.15039-0.830078 7.08008 1.85547 9.81445l11.4746 11.5723c2.44141 2.44141 4.49219 2.53906 7.32422 2.05078l5.32227-0.976562 3.32031 3.36914-0.195312 2.7832c-0.195312 2.49023 0.439453 4.39453 2.88086 6.78711l3.80859 3.71094c2.39258 2.39258 5.46875 2.53906 7.8125 0.195312l14.5508-14.5996c2.34375-2.34375 2.24609-5.32227-0.146484-7.71484l-3.85742-3.80859c-2.39258-2.39258-4.24805-3.17383-6.64062-2.97852l-2.88086 0.244141-3.22266-3.17383 1.2207-5.61523c0.634766-2.83203-0.146484-5.0293-3.07617-7.95898l-10.9863-10.9375c-16.6992-16.6016-38.8672-16.2109-53.3203-1.75781Zm7.4707 1.85547c12.1582-8.88672 28.6133-7.37305 39.7461 3.75977l12.1582 12.0605c1.17188 1.17188 1.36719 2.09961 1.02539 3.80859l-1.61133 7.42188 7.51953 7.42188 4.93164-0.292969c1.26953-0.0488281 1.66016 0.0488281 2.63672 1.02539l2.88086 2.88086-12.207 12.207-2.88086-2.88086c-0.976562-0.976562-1.12305-1.36719-1.07422-2.68555l0.341797-4.88281-7.4707-7.42188-7.61719 1.26953c-1.61133 0.341797-2.34375 0.195312-3.56445-0.976562l-10.0098-10.0098c-1.26953-1.17188-1.41602-2.00195-0.634766-3.85742l4.39453-10.4492c-7.8125-7.27539-17.9688-10.4004-28.125-7.42188-0.78125 0.195312-1.07422-0.439453-0.439453-0.976562Z"/>
</g>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;font-weight:bold;" transform="matrix(1 0 0 1 1289 1953)">Exporting</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 1289 1971)">Symbols should be outlined when exporting to ensure the</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 1289 1989)">design is preserved when submitting to Xcode.</text>
<text id="template-version" style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:end;" transform="matrix(1 0 0 1 3036 1933)">Template v.6.0</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:end;" transform="matrix(1 0 0 1 3036 1951)">Requires Xcode 16 or greater</text>
<text id="descriptive-name" style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:end;" transform="matrix(1 0 0 1 3036 1969)">Generated from </text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:end;" transform="matrix(1 0 0 1 3036 1987)">Typeset at 100.0 points</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 726)">Small</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 1156)">Medium</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 1586)">Large</text>
</g>
<g id="Guides">
<g id="H-reference" style="fill:#27AAE1;stroke:none;" transform="matrix(1 0 0 1 339 696)">
<path d="M0.993654 0L3.63775 0L29.3281-67.1323L30.0303-67.1323L30.0303-70.459L28.1226-70.459ZM11.6885-24.4799L46.9815-24.4799L46.2315-26.7285L12.4385-26.7285ZM55.1196 0L57.7637 0L30.6382-70.459L29.4326-70.459L29.4326-67.1323Z"/>
</g>
<line id="Baseline-S" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="696" y2="696"/>
<line id="Capline-S" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="625.541" y2="625.541"/>
<g id="H-reference" style="fill:#27AAE1;stroke:none;" transform="matrix(1 0 0 1 339 1126)">
<path d="M0.993654 0L3.63775 0L29.3281-67.1323L30.0303-67.1323L30.0303-70.459L28.1226-70.459ZM11.6885-24.4799L46.9815-24.4799L46.2315-26.7285L12.4385-26.7285ZM55.1196 0L57.7637 0L30.6382-70.459L29.4326-70.459L29.4326-67.1323Z"/>
</g>
<line id="Baseline-M" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="1126" y2="1126"/>
<line id="Capline-M" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="1055.54" y2="1055.54"/>
<g id="H-reference" style="fill:#27AAE1;stroke:none;" transform="matrix(1 0 0 1 339 1556)">
<path d="M0.993654 0L3.63775 0L29.3281-67.1323L30.0303-67.1323L30.0303-70.459L28.1226-70.459ZM11.6885-24.4799L46.9815-24.4799L46.2315-26.7285L12.4385-26.7285ZM55.1196 0L57.7637 0L30.6382-70.459L29.4326-70.459L29.4326-67.1323Z"/>
</g>
<line id="Baseline-L" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="1556" y2="1556"/>
<line id="Capline-L" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="1485.54" y2="1485.54"/>
<line id="right-margin-Black-S" style="fill:none;stroke:#FF3B30;stroke-width:0.5;opacity:1.0;" x1="2989.45" x2="2989.45" y1="600.785" y2="720.121"/>
<line id="left-margin-Black-S" style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="2877.35" x2="2877.35" y1="600.785" y2="720.121"/>
<line id="right-margin-Regular-S" style="fill:none;stroke:#FF3B30;stroke-width:0.5;opacity:1.0;" x1="1504.9" x2="1504.9" y1="600.785" y2="720.121"/>
<line id="left-margin-Regular-S" style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="1394.79" x2="1394.79" y1="600.785" y2="720.121"/>
<line id="right-margin-Ultralight-S" style="fill:none;stroke:#FF3B30;stroke-width:0.5;opacity:1.0;" x1="613.72" x2="613.72" y1="600.785" y2="720.121"/>
<line id="left-margin-Ultralight-S" style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="505.702" x2="505.702" y1="600.785" y2="720.121"/>
</g>
<g id="Symbols">
<g id="Black-S" transform="matrix(1 0 0 1 2877.35 696)">
<path class="monochrome-0 multicolor-0:tintColor hierarchical-0:secondary SFSymbolsPreviewWireframe" d="M9.76562-12.3047C9.76562-2.73438 15.4297 2.92969 25 2.92969L87.1094 2.92969C96.6797 2.92969 102.344-2.73438 102.344-12.3047L102.344-58.1055C102.344-67.6758 96.6797-73.3398 87.1094-73.3398L25-73.3398C15.4297-73.3398 9.76562-67.6758 9.76562-58.1055ZM23.9258-14.5508L23.9258-50.8789L88.1836-50.8789L88.1836-14.5508C88.1836-12.1094 87.1094-11.2305 84.8633-11.2305L27.2461-11.2305C25-11.2305 23.9258-12.1094 23.9258-14.5508Z"/>
<path class="monochrome-1 multicolor-1:tintColor hierarchical-1:primary SFSymbolsPreviewWireframe" d="M28.4668-57.8613C26.0742-57.8613 24.1699-59.7656 24.1699-62.1582C24.1699-64.502 26.0742-66.4062 28.4668-66.4062C30.8105-66.4062 32.7148-64.502 32.7148-62.1582C32.7148-59.7656 30.8105-57.8613 28.4668-57.8613Z"/>
<path class="monochrome-2 multicolor-2:tintColor hierarchical-2:primary SFSymbolsPreviewWireframe" d="M41.1621-57.8613C38.7695-57.8613 36.8652-59.7656 36.8652-62.1582C36.8652-64.502 38.7695-66.4062 41.1621-66.4062C43.5059-66.4062 45.4102-64.502 45.4102-62.1582C45.4102-59.7656 43.5059-57.8613 41.1621-57.8613Z"/>
<path class="monochrome-3 multicolor-3:tintColor hierarchical-3:primary SFSymbolsPreviewWireframe" d="M53.8574-57.8613C51.4648-57.8613 49.5605-59.7656 49.5605-62.1582C49.5605-64.502 51.4648-66.4062 53.8574-66.4062C56.2012-66.4062 58.1055-64.502 58.1055-62.1582C58.1055-59.7656 56.2012-57.8613 53.8574-57.8613Z"/>
<path class="monochrome-4 multicolor-4:tintColor hierarchical-4:primary SFSymbolsPreviewWireframe" d="M100.89 20.249C115.831 20.249 128.185 7.8467 128.185-7.0947C128.185-22.0361 115.831-34.3408 100.89-34.3408C85.9486-34.3408 73.5951-22.0361 73.5951-7.0947C73.5951 7.8467 85.9486 20.249 100.89 20.249Z"/>
<path class="monochrome-5 multicolor-5:systemRedColor hierarchical-5:primary SFSymbolsPreviewWireframe" d="M100.89 13.9014C112.365 13.9014 121.837 4.331 121.837-7.0947C121.837-18.5205 112.365-27.9932 100.89-27.9932C89.4154-27.9932 79.9427-18.5205 79.9427-7.0947C79.9427 4.331 89.4154 13.9014 100.89 13.9014Z"/>
<path class="monochrome-6 multicolor-6:white hierarchical-6:primary SFSymbolsPreviewWireframe" d="M96.4955 2.915C95.0794 4.3799 92.4427 4.2822 90.9779 2.8662C89.4642 1.4502 89.4642-1.2354 90.929-2.6514L95.3724-7.0459L90.9779-11.4404C89.513-12.9053 89.513-15.4443 90.9779-16.958C92.4427-18.4717 95.0306-18.4229 96.4955-16.958L100.89-12.5635L105.333-17.0069C106.798-18.4717 109.288-18.4229 110.753-16.9092C112.218-15.3955 112.316-12.9541 110.851-11.4893L106.408-7.0459L110.753-2.7002C112.267-1.1865 112.267 1.2549 110.753 2.7685C109.24 4.2822 106.749 4.331 105.236 2.8174L100.89-1.5283L96.4955 2.915Z"/>
</g>
<g id="Regular-S" transform="matrix(1 0 0 1 1394.79 696)">
<path class="monochrome-0 multicolor-0:tintColor hierarchical-0:secondary SFSymbolsPreviewWireframe" d="M9.76562-11.9141C9.76562-3.71094 13.916 0.390625 22.2168 0.390625L87.8906 0.390625C96.2402 0.390625 100.342-3.71094 100.342-11.9141L100.342-58.4961C100.342-66.6992 96.2402-70.8008 87.8906-70.8008L22.2168-70.8008C13.916-70.8008 9.76562-66.6992 9.76562-58.4961ZM16.7969-12.2559L16.7969-50.7812L93.3105-50.7812L93.3105-12.2559C93.3105-8.54492 91.3086-6.68945 87.793-6.68945L22.3145-6.68945C18.7988-6.68945 16.7969-8.54492 16.7969-12.2559Z"/>
<path class="monochrome-1 multicolor-1:tintColor hierarchical-1:primary SFSymbolsPreviewWireframe" d="M22.4609-57.1289C20.459-57.1289 18.8477-58.8867 18.8477-60.791C18.8477-62.6953 20.459-64.4043 22.4609-64.4043C24.4629-64.4043 26.123-62.6953 26.123-60.791C26.123-58.8867 24.4629-57.1289 22.4609-57.1289Z"/>
<path class="monochrome-2 multicolor-2:tintColor hierarchical-2:primary SFSymbolsPreviewWireframe" d="M34.0332-57.1289C31.9824-57.1289 30.3711-58.8867 30.3711-60.791C30.3711-62.6953 31.9824-64.4043 34.0332-64.4043C36.0352-64.4043 37.6465-62.6953 37.6465-60.791C37.6465-58.8867 36.0352-57.1289 34.0332-57.1289Z"/>
<path class="monochrome-3 multicolor-3:tintColor hierarchical-3:primary SFSymbolsPreviewWireframe" d="M45.5566-57.1289C43.5547-57.1289 41.8945-58.8867 41.8945-60.791C41.8945-62.6953 43.5547-64.4043 45.5566-64.4043C47.5586-64.4043 49.1699-62.6953 49.1699-60.791C49.1699-58.8867 47.5586-57.1289 45.5566-57.1289Z"/>
<path class="monochrome-4 multicolor-4:tintColor hierarchical-4:primary SFSymbolsPreviewWireframe" d="M99.099 18.3447C112.917 18.3447 124.49 6.82128 124.49-7.04592C124.49-20.9619 113.015-32.4365 99.099-32.4365C85.1342-32.4365 73.7084-20.9619 73.7084-7.04592C73.7084 6.96778 85.0854 18.3447 99.099 18.3447Z"/>
<path class="monochrome-5 multicolor-5:systemRedColor hierarchical-5:primary SFSymbolsPreviewWireframe" d="M99.099 12.6807C109.744 12.6807 118.777 3.69628 118.777-7.04592C118.777-17.8369 109.89-26.7236 99.099-26.7236C88.308-26.7236 79.3725-17.8369 79.3725-7.04592C79.3725 3.79398 88.308 12.6807 99.099 12.6807Z"/>
<path class="monochrome-6 multicolor-6:white hierarchical-6:primary SFSymbolsPreviewWireframe" d="M93.8256 2.08498C92.7514 3.15918 91.1401 2.96388 90.1147 1.93848C89.1381 0.913077 88.894-0.698223 89.9682-1.77242L95.3881-7.19242L90.4565-12.124C89.5288-13.1494 89.5288-14.7607 90.4565-15.6396C91.433-16.5674 92.9955-16.6162 94.0209-15.6396L98.9526-10.7568L104.324-16.1279C105.447-17.2509 107.058-17.0068 108.083-15.9814C109.06-15.0049 109.304-13.3935 108.181-12.2705L102.81-6.89942L107.742-1.91892C108.669-0.942423 108.669 0.668977 107.742 1.59668C106.765 2.52438 105.203 2.57328 104.177 1.59668L99.2456-3.33492L93.8256 2.08498Z"/>
</g>
<g id="Ultralight-S" transform="matrix(1 0 0 1 505.702 696)">
<path class="monochrome-0 multicolor-0:tintColor hierarchical-0:secondary SFSymbolsPreviewWireframe" d="M9.76562-11.5508C9.76562-5.02782 13.2349-1.562 19.7647-1.562L88.2539-1.562C94.7871-1.562 98.2529-5.11864 98.2529-11.5508L98.2529-58.7686C98.2529-65.2007 94.7871-68.7573 88.2539-68.7573L19.7647-68.7573C13.2349-68.7573 9.76562-65.2915 9.76562-58.7686ZM11.9834-11.6655L11.9834-52.5522L96.0351-52.5522L96.0351-11.6655C96.0351-6.45607 93.1704-3.78323 88.1562-3.78323L19.8623-3.78323C14.6665-3.78323 11.9834-6.45607 11.9834-11.6655Z"/>
<path class="monochrome-1 multicolor-1:tintColor hierarchical-1:primary SFSymbolsPreviewWireframe" d="M19.3731-58.2642C18.0523-58.2642 16.9404-59.3862 16.9404-60.6548C16.9404-61.9688 18.0523-63.042 19.3731-63.042C20.6485-63.042 21.7637-61.9688 21.7637-60.6548C21.7637-59.3862 20.6485-58.2642 19.3731-58.2642Z"/>
<path class="monochrome-2 multicolor-2:tintColor hierarchical-2:primary SFSymbolsPreviewWireframe" d="M27.8121-58.2642C26.4878-58.2642 25.4214-59.3408 25.4214-60.6548C25.4214-61.9688 26.4878-63.042 27.8121-63.042C29.1329-63.042 30.1993-61.9688 30.1993-60.6548C30.1993-59.3408 29.1329-58.2642 27.8121-58.2642Z"/>
<path class="monochrome-3 multicolor-3:tintColor hierarchical-3:primary SFSymbolsPreviewWireframe" d="M36.293-58.3096C34.9722-58.3096 33.857-59.3408 33.857-60.6548C33.857-61.9688 34.9722-63.042 36.293-63.042C37.5684-63.042 38.6803-61.9688 38.6803-60.6548C38.6803-59.3408 37.5684-58.3096 36.293-58.3096Z"/>
<path class="monochrome-4 multicolor-4:tintColor hierarchical-4:primary SFSymbolsPreviewWireframe" d="M97.2163 14.3033C108.9 14.3033 118.565 4.68695 118.565-7.04595C118.565-18.7822 108.953-28.395 97.2163-28.395C85.5219-28.395 75.8671-18.7368 75.8671-7.04595C75.8671 4.69725 85.5185 14.3033 97.2163 14.3033Z"/>
<path class="monochrome-5 multicolor-5:systemRedColor hierarchical-5:primary SFSymbolsPreviewWireframe" d="M97.2163 11.273C107.225 11.273 115.486 3.01515 115.486-7.04595C115.486-17.0649 107.281-25.3159 97.2163-25.3159C87.1972-25.3159 78.9428-17.0195 78.9428-7.04595C78.9428 3.02195 87.1972 11.273 97.2163 11.273Z"/>
<path class="monochrome-6 multicolor-6:white hierarchical-6:primary SFSymbolsPreviewWireframe" d="M90.6713 1.72165C90.142 2.29635 89.1664 2.37355 88.5043 1.71135C87.8457 1.04925 87.9194 0.164552 88.5395-0.455648L95.1401-7.05615L88.8915-13.3047C88.3271-13.9214 88.1909-14.8061 88.8915-15.458C89.5957-16.0679 90.477-15.9805 91.0029-15.458L97.2968-9.21295L103.849-15.81C104.427-16.3882 105.357-16.3711 106.019-15.7998C106.678-15.1865 106.649-14.2109 106.026-13.5874L99.4287-7.03565L105.677-0.829148C106.287-0.215848 106.332 0.623552 105.677 1.32415C105.018 2.02485 104.092 1.93745 103.52 1.32415L97.3173-4.87895L90.6713 1.72165Z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -0,0 +1,12 @@
{
"info" : {
"author" : "xcode",
"version" : 1
},
"symbols" : [
{
"filename" : "custom.spigot.badge.xmark.svg",
"idiom" : "universal"
}
]
}

View File

@ -0,0 +1,119 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--Generator: Apple Native CoreSVG 341-->
<!DOCTYPE svg
PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 3300 2200">
<!--glyph: "", point size: 100.0, font version: "20.0d10e1", template writer version: "138.0.0"-->
<style>.monochrome-0 {-sfsymbols-motion-group:1;-sfsymbols-layer-tags:-8a1b4e545d67453 39c0f5e3f7db3502 spigot}
.monochrome-1 {opacity:0.0;-sfsymbols-clear-behind:true;-sfsymbols-motion-group:0;-sfsymbols-layer-tags:39c0f5e3f7db3502 _badge xmark}
.monochrome-2 {-sfsymbols-motion-group:0;-sfsymbols-always-pulses:true;-sfsymbols-layer-tags:39c0f5e3f7db3502 _badge xmark}
.monochrome-3 {opacity:0.0;-sfsymbols-clear-behind:true;-sfsymbols-motion-group:0;-sfsymbols-always-pulses:true;-sfsymbols-layer-tags:39c0f5e3f7db3502 _badge xmark}
.multicolor-0:tintColor {-sfsymbols-motion-group:1;-sfsymbols-layer-tags:-8a1b4e545d67453 39c0f5e3f7db3502 spigot}
.multicolor-1:tintColor {opacity:0.0;-sfsymbols-clear-behind:true;-sfsymbols-motion-group:0;-sfsymbols-layer-tags:39c0f5e3f7db3502 _badge xmark}
.multicolor-2:systemRedColor {-sfsymbols-motion-group:0;-sfsymbols-always-pulses:true;-sfsymbols-layer-tags:39c0f5e3f7db3502 _badge xmark}
.multicolor-3:white {-sfsymbols-motion-group:0;-sfsymbols-always-pulses:true;-sfsymbols-layer-tags:39c0f5e3f7db3502 _badge xmark}
.hierarchical-0:secondary {-sfsymbols-motion-group:1;-sfsymbols-layer-tags:-8a1b4e545d67453 39c0f5e3f7db3502 spigot}
.hierarchical-1:primary {opacity:0.0;-sfsymbols-clear-behind:true;-sfsymbols-motion-group:0;-sfsymbols-layer-tags:39c0f5e3f7db3502 _badge xmark}
.hierarchical-2:primary {-sfsymbols-motion-group:0;-sfsymbols-layer-tags:39c0f5e3f7db3502 _badge xmark}
.hierarchical-3:primary {opacity:0.0;-sfsymbols-clear-behind:true;-sfsymbols-motion-group:0;-sfsymbols-layer-tags:39c0f5e3f7db3502 _badge xmark}
.SFSymbolsPreviewWireframe {fill:none;opacity:1.0;stroke:black;stroke-width:0.5}
</style>
<g id="Notes">
<rect height="2200" id="artboard" style="fill:white;opacity:1" width="3300" x="0" y="0"/>
<line style="fill:none;stroke:black;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="292" y2="292"/>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;font-weight:bold;" transform="matrix(1 0 0 1 263 322)">Weight/Scale Variations</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 559.711 322)">Ultralight</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 856.422 322)">Thin</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 1153.13 322)">Light</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 1449.84 322)">Regular</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 1746.56 322)">Medium</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 2043.27 322)">Semibold</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 2339.98 322)">Bold</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 2636.69 322)">Heavy</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 2933.4 322)">Black</text>
<line style="fill:none;stroke:black;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="1903" y2="1903"/>
<g transform="matrix(0.2 0 0 0.2 263 1933)">
<path d="m46.2402 4.15039c21.7773 0 39.4531-17.627 39.4531-39.4043s-17.6758-39.4043-39.4531-39.4043c-21.7285 0-39.4043 17.627-39.4043 39.4043s17.6758 39.4043 39.4043 39.4043Zm0-7.42188c-17.6758 0-31.9336-14.3066-31.9336-31.9824s14.2578-31.9824 31.9336-31.9824 31.9824 14.3066 31.9824 31.9824-14.3066 31.9824-31.9824 31.9824Zm-17.9688-31.9824c0 2.14844 1.51367 3.61328 3.75977 3.61328h10.498v10.5957c0 2.19727 1.46484 3.71094 3.61328 3.71094 2.24609 0 3.71094-1.51367 3.71094-3.71094v-10.5957h10.5957c2.19727 0 3.71094-1.46484 3.71094-3.61328 0-2.19727-1.51367-3.71094-3.71094-3.71094h-10.5957v-10.5469c0-2.24609-1.46484-3.75977-3.71094-3.75977-2.14844 0-3.61328 1.51367-3.61328 3.75977v10.5469h-10.498c-2.24609 0-3.75977 1.51367-3.75977 3.71094Z"/>
</g>
<g transform="matrix(0.2 0 0 0.2 281.506 1933)">
<path d="m58.5449 14.5508c27.4902 0 49.8047-22.3145 49.8047-49.8047s-22.3145-49.8047-49.8047-49.8047-49.8047 22.3145-49.8047 49.8047 22.3145 49.8047 49.8047 49.8047Zm0-8.30078c-22.9492 0-41.5039-18.5547-41.5039-41.5039s18.5547-41.5039 41.5039-41.5039 41.5039 18.5547 41.5039 41.5039-18.5547 41.5039-41.5039 41.5039Zm-22.6562-41.5039c0 2.39258 1.66016 4.00391 4.15039 4.00391h14.3555v14.4043c0 2.44141 1.66016 4.15039 4.05273 4.15039 2.44141 0 4.15039-1.66016 4.15039-4.15039v-14.4043h14.4043c2.44141 0 4.15039-1.61133 4.15039-4.00391 0-2.44141-1.70898-4.15039-4.15039-4.15039h-14.4043v-14.3555c0-2.49023-1.70898-4.19922-4.15039-4.19922-2.39258 0-4.05273 1.70898-4.05273 4.19922v14.3555h-14.3555c-2.49023 0-4.15039 1.70898-4.15039 4.15039Z"/>
</g>
<g transform="matrix(0.2 0 0 0.2 304.924 1933)">
<path d="m74.8535 28.3203c35.1074 0 63.623-28.4668 63.623-63.5742s-28.5156-63.623-63.623-63.623-63.5742 28.5156-63.5742 63.623 28.4668 63.5742 63.5742 63.5742Zm0-9.08203c-30.127 0-54.4922-24.3652-54.4922-54.4922s24.3652-54.4922 54.4922-54.4922 54.4922 24.3652 54.4922 54.4922-24.3652 54.4922-54.4922 54.4922Zm-28.8574-54.4922c0 2.58789 1.85547 4.39453 4.58984 4.39453h19.7266v19.7754c0 2.68555 1.85547 4.58984 4.44336 4.58984 2.68555 0 4.54102-1.85547 4.54102-4.58984v-19.7754h19.7754c2.68555 0 4.58984-1.80664 4.58984-4.39453 0-2.73438-1.85547-4.58984-4.58984-4.58984h-19.7754v-19.7266c0-2.73438-1.85547-4.63867-4.54102-4.63867-2.58789 0-4.44336 1.9043-4.44336 4.63867v19.7266h-19.7266c-2.73438 0-4.58984 1.85547-4.58984 4.58984Z"/>
</g>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;font-weight:bold;" transform="matrix(1 0 0 1 263 1953)">Design Variations</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 1971)">Symbols are supported in up to nine weights and three scales.</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 1989)">For optimal layout with text and other symbols, vertically align</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 2007)">symbols with the adjacent text.</text>
<line style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="776" x2="776" y1="1919" y2="1933"/>
<g transform="matrix(0.2 0 0 0.2 776 1933)">
<path d="m16.5527 0.78125c2.58789 0 3.85742-0.976562 4.78516-3.71094l6.29883-17.2363h28.8086l6.29883 17.2363c0.927734 2.73438 2.19727 3.71094 4.73633 3.71094 2.58789 0 4.24805-1.5625 4.24805-4.00391 0-0.830078-0.146484-1.61133-0.537109-2.63672l-22.9004-60.9863c-1.12305-2.97852-3.125-4.49219-6.25-4.49219-3.02734 0-5.07812 1.46484-6.15234 4.44336l-22.9004 61.084c-0.390625 1.02539-0.537109 1.80664-0.537109 2.63672 0 2.44141 1.5625 3.95508 4.10156 3.95508Zm13.4766-28.3691 11.8652-32.8613h0.244141l11.8652 32.8613Z"/>
</g>
<line style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="792.836" x2="792.836" y1="1919" y2="1933"/>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;font-weight:bold;" transform="matrix(1 0 0 1 776 1953)">Margins</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 776 1971)">Leading and trailing margins on the left and right side of each symbol</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 776 1989)">can be adjusted by modifying the x-location of the margin guidelines.</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 776 2007)">Modifications are automatically applied proportionally to all</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 776 2025)">scales and weights.</text>
<g transform="matrix(0.2 0 0 0.2 1289 1933)">
<path d="m14.209 9.32617 8.49609 8.54492c4.29688 4.3457 9.22852 4.05273 13.8672-1.07422l53.4668-58.9355-4.83398-4.88281-53.0762 58.3984c-1.75781 2.00195-3.41797 2.49023-5.76172 0.146484l-5.85938-5.81055c-2.34375-2.29492-1.80664-4.00391 0.195312-5.81055l57.373-54.0039-4.88281-4.83398-57.959 54.4434c-4.93164 4.58984-5.32227 9.47266-1.02539 13.8184Zm32.0801-90.9668c-2.09961 2.05078-2.24609 4.93164-1.07422 6.88477 1.17188 1.80664 3.4668 2.97852 6.68945 2.14844 7.32422-1.70898 14.9414-2.00195 22.0703 2.68555l-2.92969 7.27539c-1.70898 4.15039-0.830078 7.08008 1.85547 9.81445l11.4746 11.5723c2.44141 2.44141 4.49219 2.53906 7.32422 2.05078l5.32227-0.976562 3.32031 3.36914-0.195312 2.7832c-0.195312 2.49023 0.439453 4.39453 2.88086 6.78711l3.80859 3.71094c2.39258 2.39258 5.46875 2.53906 7.8125 0.195312l14.5508-14.5996c2.34375-2.34375 2.24609-5.32227-0.146484-7.71484l-3.85742-3.80859c-2.39258-2.39258-4.24805-3.17383-6.64062-2.97852l-2.88086 0.244141-3.22266-3.17383 1.2207-5.61523c0.634766-2.83203-0.146484-5.0293-3.07617-7.95898l-10.9863-10.9375c-16.6992-16.6016-38.8672-16.2109-53.3203-1.75781Zm7.4707 1.85547c12.1582-8.88672 28.6133-7.37305 39.7461 3.75977l12.1582 12.0605c1.17188 1.17188 1.36719 2.09961 1.02539 3.80859l-1.61133 7.42188 7.51953 7.42188 4.93164-0.292969c1.26953-0.0488281 1.66016 0.0488281 2.63672 1.02539l2.88086 2.88086-12.207 12.207-2.88086-2.88086c-0.976562-0.976562-1.12305-1.36719-1.07422-2.68555l0.341797-4.88281-7.4707-7.42188-7.61719 1.26953c-1.61133 0.341797-2.34375 0.195312-3.56445-0.976562l-10.0098-10.0098c-1.26953-1.17188-1.41602-2.00195-0.634766-3.85742l4.39453-10.4492c-7.8125-7.27539-17.9688-10.4004-28.125-7.42188-0.78125 0.195312-1.07422-0.439453-0.439453-0.976562Z"/>
</g>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;font-weight:bold;" transform="matrix(1 0 0 1 1289 1953)">Exporting</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 1289 1971)">Symbols should be outlined when exporting to ensure the</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 1289 1989)">design is preserved when submitting to Xcode.</text>
<text id="template-version" style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:end;" transform="matrix(1 0 0 1 3036 1933)">Template v.6.0</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:end;" transform="matrix(1 0 0 1 3036 1951)">Requires Xcode 16 or greater</text>
<text id="descriptive-name" style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:end;" transform="matrix(1 0 0 1 3036 1969)">Generated from </text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:end;" transform="matrix(1 0 0 1 3036 1987)">Typeset at 100.0 points</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 726)">Small</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 1156)">Medium</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 1586)">Large</text>
</g>
<g id="Guides">
<g id="H-reference" style="fill:#27AAE1;stroke:none;" transform="matrix(1 0 0 1 339 696)">
<path d="M0.993654 0L3.63775 0L29.3281-67.1323L30.0303-67.1323L30.0303-70.459L28.1226-70.459ZM11.6885-24.4799L46.9815-24.4799L46.2315-26.7285L12.4385-26.7285ZM55.1196 0L57.7637 0L30.6382-70.459L29.4326-70.459L29.4326-67.1323Z"/>
</g>
<line id="Baseline-S" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="696" y2="696"/>
<line id="Capline-S" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="625.541" y2="625.541"/>
<g id="H-reference" style="fill:#27AAE1;stroke:none;" transform="matrix(1 0 0 1 339 1126)">
<path d="M0.993654 0L3.63775 0L29.3281-67.1323L30.0303-67.1323L30.0303-70.459L28.1226-70.459ZM11.6885-24.4799L46.9815-24.4799L46.2315-26.7285L12.4385-26.7285ZM55.1196 0L57.7637 0L30.6382-70.459L29.4326-70.459L29.4326-67.1323Z"/>
</g>
<line id="Baseline-M" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="1126" y2="1126"/>
<line id="Capline-M" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="1055.54" y2="1055.54"/>
<g id="H-reference" style="fill:#27AAE1;stroke:none;" transform="matrix(1 0 0 1 339 1556)">
<path d="M0.993654 0L3.63775 0L29.3281-67.1323L30.0303-67.1323L30.0303-70.459L28.1226-70.459ZM11.6885-24.4799L46.9815-24.4799L46.2315-26.7285L12.4385-26.7285ZM55.1196 0L57.7637 0L30.6382-70.459L29.4326-70.459L29.4326-67.1323Z"/>
</g>
<line id="Baseline-L" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="1556" y2="1556"/>
<line id="Capline-L" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="1485.54" y2="1485.54"/>
<line id="right-margin-Black-S" style="fill:none;stroke:#FF3B30;stroke-width:0.5;opacity:1.0;" x1="2993.82" x2="2993.82" y1="600.785" y2="720.121"/>
<line id="left-margin-Black-S" style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="2872.98" x2="2872.98" y1="600.785" y2="720.121"/>
<line id="right-margin-Regular-S" style="fill:none;stroke:#FF3B30;stroke-width:0.5;opacity:1.0;" x1="1507.14" x2="1507.14" y1="600.785" y2="720.121"/>
<line id="left-margin-Regular-S" style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="1392.54" x2="1392.54" y1="600.785" y2="720.121"/>
<line id="right-margin-Ultralight-S" style="fill:none;stroke:#FF3B30;stroke-width:0.5;opacity:1.0;" x1="609.518" x2="609.518" y1="600.785" y2="720.121"/>
<line id="left-margin-Ultralight-S" style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="509.904" x2="509.904" y1="600.785" y2="720.121"/>
</g>
<g id="Symbols">
<g id="Black-S" transform="matrix(1 0 0 1 2872.98 696)">
<path class="monochrome-0 multicolor-0:tintColor hierarchical-0:secondary SFSymbolsPreviewWireframe" d="M55.0781-73.3398L33.0078-75.6348C28.3691-76.123 24.9512-73.0469 24.9512-68.6035C24.9512-64.1113 28.3691-61.0352 33.0078-61.5234L55.0781-63.8184ZM60.0098-59.2285C65.1855-59.2285 69.3848-63.4277 69.3848-68.6035C69.3848-73.7305 65.1855-77.9297 60.0098-77.9297C54.8828-77.9297 50.6836-73.7305 50.6836-68.6035C50.6836-63.4277 54.8828-59.2285 60.0098-59.2285ZM65.0391-73.3398L65.0391-63.8184L87.1094-61.5234C91.748-61.0352 95.166-64.1113 95.166-68.6035C95.166-73.0469 91.748-76.123 87.1094-75.6348ZM54.3945-62.5488L54.3945-50.6836L65.625-50.6836L65.625-62.5488ZM64.2578-41.1133L73.7793-41.1133L73.7793-48.7793C73.7793-53.4668 70.3613-56.8848 65.6738-56.8848L54.3457-56.8848C49.6582-56.8848 46.2402-53.4668 46.2402-48.7793L46.2402-41.1133L55.7617-41.1133L55.7617-47.8027C55.7617-48.1445 55.957-48.3398 56.2988-48.3398L63.7207-48.3398C64.0625-48.3398 64.2578-48.1445 64.2578-47.8027ZM60.0098 0.78125C70.6055 0.78125 74.3164-4.3457 77.3926-7.12891C78.5645-8.1543 79.6387-8.88672 80.957-8.88672C83.1543-8.88672 84.9609-6.88477 84.9609-3.66211L84.9609 1.70898C84.9609 4.93164 87.4512 7.42188 90.6738 7.42188L105.371 7.42188C108.594 7.42188 111.084 4.93164 111.084 1.70898L111.084-4.15039C111.084-27.5879 96.0449-35.2539 83.3496-35.2539L83.252-35.2539C81.6895-35.2539 80.3711-36.1328 79.834-37.6465L78.7598-40.6738C77.5879-43.9453 75.5371-45.5566 72.3633-45.5566L47.6562-45.5566C44.4824-45.5566 42.4316-43.9453 41.2598-40.6738L40.1855-37.6465C39.6484-36.1328 38.3301-35.2539 36.7676-35.2539L12.5977-35.2539L12.5977-25.2441L40.4785-25.2441C45.4102-25.2441 47.168-27.6367 48.3398-30.8105L50.0488-35.5469L69.9707-35.5469L71.6797-30.8105C72.8516-27.6367 75.0977-25.2441 80.0293-25.2441L82.1289-25.2441C92.1875-25.2441 101.123-20.3613 101.123-4.15039L101.123-2.58789L94.9707-2.58789L94.9707-3.66211C94.9707-16.2598 86.9629-18.8965 81.25-18.8965C76.6602-18.8965 73.8281-17.3828 71.582-15.5762C68.0664-12.7441 65.7715-9.22852 60.0098-9.22852C56.1523-9.22852 52.8809-10.5469 49.5605-14.502C47.0703-17.4316 45.6543-18.8965 39.7461-18.8965L12.5977-18.8965L12.5977-8.88672L39.3555-8.88672C40.625-8.88672 41.748-8.30078 42.5781-7.27539C46.9727-2.09961 53.125 0.78125 60.0098 0.78125ZM15.1367 0.634766C18.0664 0.634766 20.5078-1.80664 20.5078-4.73633L20.5078-39.4043C20.5078-42.334 18.0664-44.7754 15.1367-44.7754C12.207-44.7754 9.76562-42.334 9.76562-39.4043L9.76562-4.73633C9.76562-1.80664 12.207 0.634766 15.1367 0.634766Z"/>
<path class="monochrome-1 multicolor-1:tintColor hierarchical-1:primary SFSymbolsPreviewWireframe" d="M108.756 20.249C123.697 20.249 136.051 7.8467 136.051-7.0947C136.051-22.0361 123.697-34.3408 108.756-34.3408C93.8146-34.3408 81.4611-22.0361 81.4611-7.0947C81.4611 7.8467 93.8146 20.249 108.756 20.249Z"/>
<path class="monochrome-2 multicolor-2:systemRedColor hierarchical-2:primary SFSymbolsPreviewWireframe" d="M108.756 13.9014C120.231 13.9014 129.703 4.331 129.703-7.0947C129.703-18.5205 120.231-27.9932 108.756-27.9932C97.2814-27.9932 87.8087-18.5205 87.8087-7.0947C87.8087 4.331 97.2814 13.9014 108.756 13.9014Z"/>
<path class="monochrome-3 multicolor-3:white hierarchical-3:primary SFSymbolsPreviewWireframe" d="M104.362 2.915C102.945 4.3799 100.309 4.2822 98.8439 2.8662C97.3302 1.4502 97.3302-1.2354 98.795-2.6514L103.238-7.0459L98.8439-11.4404C97.379-12.9053 97.379-15.4443 98.8439-16.958C100.309-18.4717 102.897-18.4229 104.362-16.958L108.756-12.5635L113.199-17.0069C114.664-18.4717 117.154-18.4229 118.619-16.9092C120.084-15.3955 120.182-12.9541 118.717-11.4893L114.274-7.0459L118.619-2.7002C120.133-1.1865 120.133 1.2549 118.619 2.7685C117.106 4.2822 114.615 4.331 113.102 2.8174L108.756-1.5283L104.362 2.915Z"/>
</g>
<g id="Regular-S" transform="matrix(1 0 0 1 1392.54 696)">
<path class="monochrome-0 multicolor-0:tintColor hierarchical-0:secondary SFSymbolsPreviewWireframe" d="M52.7344-68.7988L32.373-70.9473C28.8086-71.3379 26.2695-69.043 26.2695-65.6738C26.2695-62.2559 28.8086-59.9609 32.373-60.3516L52.7344-62.5ZM56.4453-58.1543C60.5957-58.1543 63.9648-61.5234 63.9648-65.6738C63.9648-69.8242 60.5957-73.1445 56.4453-73.1445C52.2949-73.1445 48.9258-69.8242 48.9258-65.6738C48.9258-61.5234 52.2949-58.1543 56.4453-58.1543ZM60.1074-68.7988L60.1074-62.5L80.4688-60.3516C84.0332-59.9609 86.5723-62.2559 86.5723-65.6738C86.5723-69.043 84.0332-71.3379 80.4688-70.9473ZM52.6367-60.9863L52.6367-51.6113L60.2051-51.6113L60.2051-60.9863ZM62.4023-43.1641L68.3105-43.1641L68.3105-49.0723C68.3105-52.7344 65.7227-55.2734 62.0605-55.2734L50.6836-55.2734C47.0215-55.2734 44.4824-52.7344 44.4824-49.0723L44.4824-43.1641L50.3418-43.1641L50.3418-48.3887C50.3418-49.5117 51.1719-50.3418 52.2949-50.3418L60.4492-50.3418C61.6211-50.3418 62.4023-49.5117 62.4023-48.3887ZM56.3965-3.56445C62.9395-3.56445 67.334-6.15234 71.8262-10.498C73.4375-12.0605 75.293-12.8418 77.0508-12.8418C80.0781-12.8418 82.5684-10.5957 82.5684-6.20117L82.5684-1.2207C82.5684 1.02539 84.1797 2.63672 86.4258 2.63672L100.977 2.63672C103.174 2.63672 104.834 1.02539 104.834-1.2207L104.834-4.6875C104.834-28.4668 89.9414-35.7422 77.7832-35.7422L76.2695-35.7422C75.8789-35.7422 75.5859-36.0352 75.3906-36.377L73.1934-42.4316C72.2656-44.9707 70.8984-46.0449 68.5547-46.0449L44.1895-46.0449C41.8945-46.0449 40.5273-44.9707 39.5508-42.4316L37.3535-36.377C37.207-36.0352 36.8652-35.7422 36.4746-35.7422L12.5-35.7422L12.5-29.3945L37.0117-29.3945C40.918-29.3945 42.1875-31.0547 43.1641-33.7891L45.3613-39.6973L67.3828-39.6973L69.5801-33.7891C70.5566-31.0547 71.8262-29.3945 75.7324-29.3945L77.7344-29.3945C88.4277-29.3945 98.5352-23.6816 98.5352-4.6875L98.5352-3.71094L88.916-3.71094L88.916-6.20117C88.916-17.1387 82.2754-19.2871 77.4902-19.2871C73.3398-19.2871 70.3125-17.627 67.3828-14.7461C65.1367-12.6465 61.8652-9.91211 56.3965-9.91211C50.7812-9.91211 47.3633-12.3047 44.873-15.3809C42.334-18.5547 40.2344-19.4824 36.1328-19.4824L12.5-19.4824L12.5-13.1348L35.7422-13.1348C37.8418-13.1348 38.7207-12.8906 40.1855-11.1816C44.043-6.5918 49.8047-3.56445 56.3965-3.56445ZM13.2812-4.10156C15.2344-4.10156 16.7969-5.71289 16.7969-7.61719L16.7969-39.9902C16.7969-41.8945 15.2344-43.5059 13.2812-43.5059C11.377-43.5059 9.76562-41.8945 9.76562-39.9902L9.76562-7.61719C9.76562-5.71289 11.377-4.10156 13.2812-4.10156Z"/>
<path class="monochrome-1 multicolor-1:tintColor hierarchical-1:primary SFSymbolsPreviewWireframe" d="M103.14 18.3447C116.958 18.3447 128.531 6.82128 128.531-7.04592C128.531-20.9619 117.056-32.4365 103.14-32.4365C89.1752-32.4365 77.7494-20.9619 77.7494-7.04592C77.7494 6.96778 89.1264 18.3447 103.14 18.3447Z"/>
<path class="monochrome-2 multicolor-2:systemRedColor hierarchical-2:primary SFSymbolsPreviewWireframe" d="M103.14 12.6807C113.785 12.6807 122.818 3.69628 122.818-7.04592C122.818-17.8369 113.931-26.7236 103.14-26.7236C92.349-26.7236 83.4135-17.8369 83.4135-7.04592C83.4135 3.79398 92.349 12.6807 103.14 12.6807Z"/>
<path class="monochrome-3 multicolor-3:white hierarchical-3:primary SFSymbolsPreviewWireframe" d="M97.8666 2.08498C96.7924 3.15918 95.1811 2.96388 94.1557 1.93848C93.1791 0.913077 92.935-0.698223 94.0092-1.77242L99.4291-7.19242L94.4975-12.124C93.5698-13.1494 93.5698-14.7607 94.4975-15.6396C95.474-16.5674 97.0365-16.6162 98.0619-15.6396L102.994-10.7568L108.365-16.1279C109.488-17.2509 111.099-17.0068 112.124-15.9814C113.101-15.0049 113.345-13.3935 112.222-12.2705L106.851-6.89942L111.783-1.91892C112.71-0.942423 112.71 0.668977 111.783 1.59668C110.806 2.52438 109.244 2.57328 108.218 1.59668L103.287-3.33492L97.8666 2.08498Z"/>
</g>
<g id="Ultralight-S" transform="matrix(1 0 0 1 509.904 696)">
<path class="monochrome-0 multicolor-0:tintColor hierarchical-0:secondary SFSymbolsPreviewWireframe" d="M46.0591-63.8038L29.1035-65.9976C26.2202-66.3882 24.0899-64.4566 24.0899-61.6324C24.0899-58.7593 26.2202-56.8731 29.1035-57.2637L46.0591-59.4575ZM48.544-56.4287C51.4683-56.4287 53.7022-58.7534 53.7022-61.6324C53.7022-64.5113 51.4683-66.8326 48.544-66.8326C45.6197-66.8326 43.295-64.5113 43.295-61.6324C43.295-58.7534 45.6197-56.4287 48.544-56.4287ZM51.0709-63.8038L51.0709-59.4575L67.9811-57.2637C70.9098-56.8731 73.0401-58.7593 73.0401-61.6324C73.0401-64.4566 70.9098-66.3882 67.9811-65.9976ZM47.0059-58.8975L47.0059-51.1118L50.1695-51.1118L50.1695-58.8975ZM55.7725-44.6172L57.9117-44.6172L57.9117-49.708C57.9117-51.5537 56.6407-52.8667 54.8404-52.8667L42.192-52.8667C40.3917-52.8667 39.0787-51.5537 39.0787-49.708L39.0787-44.6172L41.2598-44.6172L41.2598-49.7056C41.2598-50.3291 41.6358-50.7505 42.2594-50.7505L54.7276-50.7505C55.3545-50.7505 55.7725-50.3291 55.7725-49.7056ZM48.4952-10.9208C55.7193-10.9208 59.3419-14.7348 63.3345-17.6274C64.7188-18.645 66.5289-19.4262 68.2413-19.4262C72.1768-19.4262 74.6671-16.726 74.6671-12.5585L74.6671-5.53463C74.6671-4.15133 75.0523-3.76606 76.3902-3.76606L88.0802-3.76606C89.3692-3.76606 89.8488-4.24215 89.8488-5.53463L89.8488-9.63717C89.8488-26.1963 81.1773-36.0146 66.4308-36.0146L64.0543-36.0146C63.3004-36.0146 62.6441-36.4438 62.4034-37.1489L59.7066-44.5205C59.3692-45.3794 58.8194-45.7725 57.8834-45.7725L39.1036-45.7725C38.1255-45.7725 37.6211-45.3794 37.2803-44.5205L34.5835-37.1489C34.3462-36.4438 33.6411-36.0146 32.8872-36.0146L10.8198-36.0146L10.8198-33.8447L32.3799-33.8447C35.0147-33.8447 36.0118-34.8691 36.7159-36.7861L39.231-43.6479L57.7105-43.6479L60.2711-36.7861C60.9298-34.8691 61.9268-33.8447 64.607-33.8447L66.4273-33.8447C79.9361-33.8447 87.7276-24.8169 87.7276-9.63717L87.7276-5.93602L76.7916-5.93602L76.7916-12.5585C76.7916-18.2285 73.4659-21.603 68.272-21.603C65.8018-21.603 63.6373-20.6694 61.979-19.3779C58.3707-16.5517 55.4625-13.0908 48.4952-13.0908C41.5631-13.0908 38.1905-16.8003 34.5196-19.6948C32.8887-21.0068 31.9698-21.3442 29.0943-21.3442L10.8198-21.3442L10.8198-19.1743L29.0669-19.1743C31.2574-19.1743 31.8184-18.9755 33.1924-17.9477C37.1407-15.038 41.3131-10.9208 48.4952-10.9208ZM10.8745-13.2289C11.4654-13.2289 11.9834-13.7504 11.9834-14.3378L11.9834-40.7168C11.9834-41.3496 11.4654-41.8257 10.8745-41.8257C10.2417-41.8257 9.76562-41.3496 9.76562-40.7168L9.76562-14.3378C9.76562-13.7504 10.2417-13.2289 10.8745-13.2289Z"/>
<path class="monochrome-1 multicolor-1:tintColor hierarchical-1:primary SFSymbolsPreviewWireframe" d="M89.6527 14.3033C101.337 14.3033 111.002 4.68695 111.002-7.04595C111.002-18.7822 101.389-28.395 89.6527-28.395C77.9583-28.395 68.3035-18.7368 68.3035-7.04595C68.3035 4.69725 77.9549 14.3033 89.6527 14.3033Z"/>
<path class="monochrome-2 multicolor-2:systemRedColor hierarchical-2:primary SFSymbolsPreviewWireframe" d="M89.6527 11.273C99.6615 11.273 107.923 3.01515 107.923-7.04595C107.923-17.0649 99.7171-25.3159 89.6527-25.3159C79.6336-25.3159 71.3792-17.0195 71.3792-7.04595C71.3792 3.02195 79.6336 11.273 89.6527 11.273Z"/>
<path class="monochrome-3 multicolor-3:white hierarchical-3:primary SFSymbolsPreviewWireframe" d="M83.1077 1.72165C82.5784 2.29635 81.6028 2.37355 80.9407 1.71135C80.2821 1.04925 80.3558 0.164552 80.9759-0.455648L87.5765-7.05615L81.3279-13.3047C80.7635-13.9214 80.6273-14.8061 81.3279-15.458C82.0321-16.0679 82.9134-15.9805 83.4393-15.458L89.7332-9.21295L96.285-15.81C96.8631-16.3882 97.7933-16.3711 98.4554-15.7998C99.1141-15.1865 99.0858-14.2109 98.4622-13.5874L91.8651-7.03565L98.1136-0.829148C98.7235-0.215848 98.7689 0.623552 98.1136 1.32415C97.4549 2.02485 96.5281 1.93745 95.9568 1.32415L89.7537-4.87895L83.1077 1.72165Z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -0,0 +1,12 @@
{
"info" : {
"author" : "xcode",
"version" : 1
},
"symbols" : [
{
"filename" : "custom.terminal.badge.xmark.svg",
"idiom" : "universal"
}
]
}

View File

@ -0,0 +1,125 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--Generator: Apple Native CoreSVG 341-->
<!DOCTYPE svg
PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 3300 2200">
<!--glyph: "", point size: 100.0, font version: "20.0d10e1", template writer version: "138.0.0"-->
<style>.monochrome-0 {-sfsymbols-motion-group:1;-sfsymbols-layer-tags:-116e57ac81a25b71 6eb21f60b3d84617 apple.terminal}
.monochrome-1 {-sfsymbols-motion-group:1;-sfsymbols-layer-tags:-116e57ac81a25b71 6eb21f60b3d84617 apple.terminal}
.monochrome-2 {opacity:0.0;-sfsymbols-clear-behind:true;-sfsymbols-motion-group:0;-sfsymbols-layer-tags:-116e57ac81a25b71 _badge xmark}
.monochrome-3 {-sfsymbols-motion-group:0;-sfsymbols-always-pulses:true;-sfsymbols-layer-tags:-116e57ac81a25b71 _badge xmark}
.monochrome-4 {opacity:0.0;-sfsymbols-clear-behind:true;-sfsymbols-motion-group:0;-sfsymbols-always-pulses:true;-sfsymbols-layer-tags:-116e57ac81a25b71 _badge xmark}
.multicolor-0:tintColor {-sfsymbols-motion-group:1;-sfsymbols-layer-tags:-116e57ac81a25b71 6eb21f60b3d84617 apple.terminal}
.multicolor-1:tintColor {-sfsymbols-motion-group:1;-sfsymbols-layer-tags:-116e57ac81a25b71 6eb21f60b3d84617 apple.terminal}
.multicolor-2:tintColor {opacity:0.0;-sfsymbols-clear-behind:true;-sfsymbols-motion-group:0;-sfsymbols-layer-tags:-116e57ac81a25b71 _badge xmark}
.multicolor-3:systemRedColor {-sfsymbols-motion-group:0;-sfsymbols-always-pulses:true;-sfsymbols-layer-tags:-116e57ac81a25b71 _badge xmark}
.multicolor-4:white {-sfsymbols-motion-group:0;-sfsymbols-always-pulses:true;-sfsymbols-layer-tags:-116e57ac81a25b71 _badge xmark}
.hierarchical-0:secondary {-sfsymbols-motion-group:1;-sfsymbols-layer-tags:-116e57ac81a25b71 6eb21f60b3d84617 apple.terminal}
.hierarchical-1:secondary {-sfsymbols-motion-group:1;-sfsymbols-layer-tags:-116e57ac81a25b71 6eb21f60b3d84617 apple.terminal}
.hierarchical-2:primary {opacity:0.0;-sfsymbols-clear-behind:true;-sfsymbols-motion-group:0;-sfsymbols-layer-tags:-116e57ac81a25b71 _badge xmark}
.hierarchical-3:primary {-sfsymbols-motion-group:0;-sfsymbols-layer-tags:-116e57ac81a25b71 _badge xmark}
.hierarchical-4:primary {opacity:0.0;-sfsymbols-clear-behind:true;-sfsymbols-motion-group:0;-sfsymbols-layer-tags:-116e57ac81a25b71 _badge xmark}
.SFSymbolsPreviewWireframe {fill:none;opacity:1.0;stroke:black;stroke-width:0.5}
</style>
<g id="Notes">
<rect height="2200" id="artboard" style="fill:white;opacity:1" width="3300" x="0" y="0"/>
<line style="fill:none;stroke:black;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="292" y2="292"/>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;font-weight:bold;" transform="matrix(1 0 0 1 263 322)">Weight/Scale Variations</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 559.711 322)">Ultralight</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 856.422 322)">Thin</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 1153.13 322)">Light</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 1449.84 322)">Regular</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 1746.56 322)">Medium</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 2043.27 322)">Semibold</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 2339.98 322)">Bold</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 2636.69 322)">Heavy</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:middle;" transform="matrix(1 0 0 1 2933.4 322)">Black</text>
<line style="fill:none;stroke:black;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="1903" y2="1903"/>
<g transform="matrix(0.2 0 0 0.2 263 1933)">
<path d="m46.2402 4.15039c21.7773 0 39.4531-17.627 39.4531-39.4043s-17.6758-39.4043-39.4531-39.4043c-21.7285 0-39.4043 17.627-39.4043 39.4043s17.6758 39.4043 39.4043 39.4043Zm0-7.42188c-17.6758 0-31.9336-14.3066-31.9336-31.9824s14.2578-31.9824 31.9336-31.9824 31.9824 14.3066 31.9824 31.9824-14.3066 31.9824-31.9824 31.9824Zm-17.9688-31.9824c0 2.14844 1.51367 3.61328 3.75977 3.61328h10.498v10.5957c0 2.19727 1.46484 3.71094 3.61328 3.71094 2.24609 0 3.71094-1.51367 3.71094-3.71094v-10.5957h10.5957c2.19727 0 3.71094-1.46484 3.71094-3.61328 0-2.19727-1.51367-3.71094-3.71094-3.71094h-10.5957v-10.5469c0-2.24609-1.46484-3.75977-3.71094-3.75977-2.14844 0-3.61328 1.51367-3.61328 3.75977v10.5469h-10.498c-2.24609 0-3.75977 1.51367-3.75977 3.71094Z"/>
</g>
<g transform="matrix(0.2 0 0 0.2 281.506 1933)">
<path d="m58.5449 14.5508c27.4902 0 49.8047-22.3145 49.8047-49.8047s-22.3145-49.8047-49.8047-49.8047-49.8047 22.3145-49.8047 49.8047 22.3145 49.8047 49.8047 49.8047Zm0-8.30078c-22.9492 0-41.5039-18.5547-41.5039-41.5039s18.5547-41.5039 41.5039-41.5039 41.5039 18.5547 41.5039 41.5039-18.5547 41.5039-41.5039 41.5039Zm-22.6562-41.5039c0 2.39258 1.66016 4.00391 4.15039 4.00391h14.3555v14.4043c0 2.44141 1.66016 4.15039 4.05273 4.15039 2.44141 0 4.15039-1.66016 4.15039-4.15039v-14.4043h14.4043c2.44141 0 4.15039-1.61133 4.15039-4.00391 0-2.44141-1.70898-4.15039-4.15039-4.15039h-14.4043v-14.3555c0-2.49023-1.70898-4.19922-4.15039-4.19922-2.39258 0-4.05273 1.70898-4.05273 4.19922v14.3555h-14.3555c-2.49023 0-4.15039 1.70898-4.15039 4.15039Z"/>
</g>
<g transform="matrix(0.2 0 0 0.2 304.924 1933)">
<path d="m74.8535 28.3203c35.1074 0 63.623-28.4668 63.623-63.5742s-28.5156-63.623-63.623-63.623-63.5742 28.5156-63.5742 63.623 28.4668 63.5742 63.5742 63.5742Zm0-9.08203c-30.127 0-54.4922-24.3652-54.4922-54.4922s24.3652-54.4922 54.4922-54.4922 54.4922 24.3652 54.4922 54.4922-24.3652 54.4922-54.4922 54.4922Zm-28.8574-54.4922c0 2.58789 1.85547 4.39453 4.58984 4.39453h19.7266v19.7754c0 2.68555 1.85547 4.58984 4.44336 4.58984 2.68555 0 4.54102-1.85547 4.54102-4.58984v-19.7754h19.7754c2.68555 0 4.58984-1.80664 4.58984-4.39453 0-2.73438-1.85547-4.58984-4.58984-4.58984h-19.7754v-19.7266c0-2.73438-1.85547-4.63867-4.54102-4.63867-2.58789 0-4.44336 1.9043-4.44336 4.63867v19.7266h-19.7266c-2.73438 0-4.58984 1.85547-4.58984 4.58984Z"/>
</g>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;font-weight:bold;" transform="matrix(1 0 0 1 263 1953)">Design Variations</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 1971)">Symbols are supported in up to nine weights and three scales.</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 1989)">For optimal layout with text and other symbols, vertically align</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 2007)">symbols with the adjacent text.</text>
<line style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="776" x2="776" y1="1919" y2="1933"/>
<g transform="matrix(0.2 0 0 0.2 776 1933)">
<path d="m16.5527 0.78125c2.58789 0 3.85742-0.976562 4.78516-3.71094l6.29883-17.2363h28.8086l6.29883 17.2363c0.927734 2.73438 2.19727 3.71094 4.73633 3.71094 2.58789 0 4.24805-1.5625 4.24805-4.00391 0-0.830078-0.146484-1.61133-0.537109-2.63672l-22.9004-60.9863c-1.12305-2.97852-3.125-4.49219-6.25-4.49219-3.02734 0-5.07812 1.46484-6.15234 4.44336l-22.9004 61.084c-0.390625 1.02539-0.537109 1.80664-0.537109 2.63672 0 2.44141 1.5625 3.95508 4.10156 3.95508Zm13.4766-28.3691 11.8652-32.8613h0.244141l11.8652 32.8613Z"/>
</g>
<line style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="792.836" x2="792.836" y1="1919" y2="1933"/>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;font-weight:bold;" transform="matrix(1 0 0 1 776 1953)">Margins</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 776 1971)">Leading and trailing margins on the left and right side of each symbol</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 776 1989)">can be adjusted by modifying the x-location of the margin guidelines.</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 776 2007)">Modifications are automatically applied proportionally to all</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 776 2025)">scales and weights.</text>
<g transform="matrix(0.2 0 0 0.2 1289 1933)">
<path d="m14.209 9.32617 8.49609 8.54492c4.29688 4.3457 9.22852 4.05273 13.8672-1.07422l53.4668-58.9355-4.83398-4.88281-53.0762 58.3984c-1.75781 2.00195-3.41797 2.49023-5.76172 0.146484l-5.85938-5.81055c-2.34375-2.29492-1.80664-4.00391 0.195312-5.81055l57.373-54.0039-4.88281-4.83398-57.959 54.4434c-4.93164 4.58984-5.32227 9.47266-1.02539 13.8184Zm32.0801-90.9668c-2.09961 2.05078-2.24609 4.93164-1.07422 6.88477 1.17188 1.80664 3.4668 2.97852 6.68945 2.14844 7.32422-1.70898 14.9414-2.00195 22.0703 2.68555l-2.92969 7.27539c-1.70898 4.15039-0.830078 7.08008 1.85547 9.81445l11.4746 11.5723c2.44141 2.44141 4.49219 2.53906 7.32422 2.05078l5.32227-0.976562 3.32031 3.36914-0.195312 2.7832c-0.195312 2.49023 0.439453 4.39453 2.88086 6.78711l3.80859 3.71094c2.39258 2.39258 5.46875 2.53906 7.8125 0.195312l14.5508-14.5996c2.34375-2.34375 2.24609-5.32227-0.146484-7.71484l-3.85742-3.80859c-2.39258-2.39258-4.24805-3.17383-6.64062-2.97852l-2.88086 0.244141-3.22266-3.17383 1.2207-5.61523c0.634766-2.83203-0.146484-5.0293-3.07617-7.95898l-10.9863-10.9375c-16.6992-16.6016-38.8672-16.2109-53.3203-1.75781Zm7.4707 1.85547c12.1582-8.88672 28.6133-7.37305 39.7461 3.75977l12.1582 12.0605c1.17188 1.17188 1.36719 2.09961 1.02539 3.80859l-1.61133 7.42188 7.51953 7.42188 4.93164-0.292969c1.26953-0.0488281 1.66016 0.0488281 2.63672 1.02539l2.88086 2.88086-12.207 12.207-2.88086-2.88086c-0.976562-0.976562-1.12305-1.36719-1.07422-2.68555l0.341797-4.88281-7.4707-7.42188-7.61719 1.26953c-1.61133 0.341797-2.34375 0.195312-3.56445-0.976562l-10.0098-10.0098c-1.26953-1.17188-1.41602-2.00195-0.634766-3.85742l4.39453-10.4492c-7.8125-7.27539-17.9688-10.4004-28.125-7.42188-0.78125 0.195312-1.07422-0.439453-0.439453-0.976562Z"/>
</g>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;font-weight:bold;" transform="matrix(1 0 0 1 1289 1953)">Exporting</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 1289 1971)">Symbols should be outlined when exporting to ensure the</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 1289 1989)">design is preserved when submitting to Xcode.</text>
<text id="template-version" style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:end;" transform="matrix(1 0 0 1 3036 1933)">Template v.6.0</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:end;" transform="matrix(1 0 0 1 3036 1951)">Requires Xcode 16 or greater</text>
<text id="descriptive-name" style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:end;" transform="matrix(1 0 0 1 3036 1969)">Generated from </text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:end;" transform="matrix(1 0 0 1 3036 1987)">Typeset at 100.0 points</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 726)">Small</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 1156)">Medium</text>
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 1586)">Large</text>
</g>
<g id="Guides">
<g id="H-reference" style="fill:#27AAE1;stroke:none;" transform="matrix(1 0 0 1 339 696)">
<path d="M0.993654 0L3.63775 0L29.3281-67.1323L30.0303-67.1323L30.0303-70.459L28.1226-70.459ZM11.6885-24.4799L46.9815-24.4799L46.2315-26.7285L12.4385-26.7285ZM55.1196 0L57.7637 0L30.6382-70.459L29.4326-70.459L29.4326-67.1323Z"/>
</g>
<line id="Baseline-S" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="696" y2="696"/>
<line id="Capline-S" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="625.541" y2="625.541"/>
<g id="H-reference" style="fill:#27AAE1;stroke:none;" transform="matrix(1 0 0 1 339 1126)">
<path d="M0.993654 0L3.63775 0L29.3281-67.1323L30.0303-67.1323L30.0303-70.459L28.1226-70.459ZM11.6885-24.4799L46.9815-24.4799L46.2315-26.7285L12.4385-26.7285ZM55.1196 0L57.7637 0L30.6382-70.459L29.4326-70.459L29.4326-67.1323Z"/>
</g>
<line id="Baseline-M" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="1126" y2="1126"/>
<line id="Capline-M" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="1055.54" y2="1055.54"/>
<g id="H-reference" style="fill:#27AAE1;stroke:none;" transform="matrix(1 0 0 1 339 1556)">
<path d="M0.993654 0L3.63775 0L29.3281-67.1323L30.0303-67.1323L30.0303-70.459L28.1226-70.459ZM11.6885-24.4799L46.9815-24.4799L46.2315-26.7285L12.4385-26.7285ZM55.1196 0L57.7637 0L30.6382-70.459L29.4326-70.459L29.4326-67.1323Z"/>
</g>
<line id="Baseline-L" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="1556" y2="1556"/>
<line id="Capline-L" style="fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="1485.54" y2="1485.54"/>
<line id="right-margin-Black-S" style="fill:none;stroke:#FF3B30;stroke-width:0.5;opacity:1.0;" x1="2989.45" x2="2989.45" y1="600.785" y2="720.121"/>
<line id="left-margin-Black-S" style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="2877.35" x2="2877.35" y1="600.785" y2="720.121"/>
<line id="right-margin-Regular-S" style="fill:none;stroke:#FF3B30;stroke-width:0.5;opacity:1.0;" x1="1504.9" x2="1504.9" y1="600.785" y2="720.121"/>
<line id="left-margin-Regular-S" style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="1394.79" x2="1394.79" y1="600.785" y2="720.121"/>
<line id="right-margin-Ultralight-S" style="fill:none;stroke:#FF3B30;stroke-width:0.5;opacity:1.0;" x1="613.72" x2="613.72" y1="600.785" y2="720.121"/>
<line id="left-margin-Ultralight-S" style="fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;" x1="505.702" x2="505.702" y1="600.785" y2="720.121"/>
</g>
<g id="Symbols">
<g id="Black-S" transform="matrix(1 0 0 1 2877.35 696)">
<path class="monochrome-0 multicolor-0:tintColor hierarchical-0:secondary SFSymbolsPreviewWireframe" d="M25 2.92969L87.1094 2.92969C96.6797 2.92969 102.344-2.73438 102.344-12.3047L102.344-58.1055C102.344-67.6758 96.6797-73.3398 87.1094-73.3398L25-73.3398C15.4297-73.3398 9.76562-67.6758 9.76562-58.1055L9.76562-12.3047C9.76562-2.73438 15.4297 2.92969 25 2.92969ZM27.2461-11.2305C25-11.2305 23.9258-12.1094 23.9258-14.5508L23.9258-55.8594C23.9258-58.3008 25-59.1797 27.2461-59.1797L84.8633-59.1797C87.1094-59.1797 88.1836-58.3008 88.1836-55.8594L88.1836-14.5508C88.1836-12.1094 87.1094-11.2305 84.8633-11.2305Z"/>
<path class="monochrome-1 multicolor-1:tintColor hierarchical-1:secondary SFSymbolsPreviewWireframe" d="M31.4941-39.9902C26.6602-37.0605 30.8105-29.6875 36.0352-32.959L45.8496-39.1602C49.0234-41.1621 48.9746-45.8496 45.8496-47.8027L36.0352-53.9551C30.8594-57.2266 26.6602-49.8535 31.4453-46.9727L37.2559-43.457ZM47.2168-34.1797C47.2168-31.9336 49.0234-30.0781 51.3184-30.0781L64.5508-30.0781C66.8457-30.0781 68.6035-31.9336 68.6035-34.1797C68.6035-36.4746 66.8457-38.2324 64.5508-38.2324L51.3184-38.2324C49.0723-38.2324 47.2168-36.4746 47.2168-34.1797Z"/>
<path class="monochrome-2 multicolor-2:tintColor hierarchical-2:primary SFSymbolsPreviewWireframe" d="M100.89 20.249C115.831 20.249 128.185 7.8467 128.185-7.0947C128.185-22.0361 115.831-34.3408 100.89-34.3408C85.9486-34.3408 73.5951-22.0361 73.5951-7.0947C73.5951 7.8467 85.9486 20.249 100.89 20.249Z"/>
<path class="monochrome-3 multicolor-3:systemRedColor hierarchical-3:primary SFSymbolsPreviewWireframe" d="M100.89 13.9014C112.365 13.9014 121.837 4.331 121.837-7.0947C121.837-18.5205 112.365-27.9932 100.89-27.9932C89.4154-27.9932 79.9427-18.5205 79.9427-7.0947C79.9427 4.331 89.4154 13.9014 100.89 13.9014Z"/>
<path class="monochrome-4 multicolor-4:white hierarchical-4:primary SFSymbolsPreviewWireframe" d="M96.4955 2.915C95.0794 4.3799 92.4427 4.2822 90.9779 2.8662C89.4642 1.4502 89.4642-1.2354 90.929-2.6514L95.3724-7.0459L90.9779-11.4404C89.513-12.9053 89.513-15.4443 90.9779-16.958C92.4427-18.4717 95.0306-18.4229 96.4955-16.958L100.89-12.5635L105.333-17.0069C106.798-18.4717 109.288-18.4229 110.753-16.9092C112.218-15.3955 112.316-12.9541 110.851-11.4893L106.408-7.0459L110.753-2.7002C112.267-1.1865 112.267 1.2549 110.753 2.7685C109.24 4.2822 106.749 4.331 105.236 2.8174L100.89-1.5283L96.4955 2.915Z"/>
</g>
<g id="Regular-S" transform="matrix(1 0 0 1 1394.79 696)">
<path class="monochrome-0 multicolor-0:tintColor hierarchical-0:secondary SFSymbolsPreviewWireframe" d="M22.2168 0.390625L87.8906 0.390625C96.2402 0.390625 100.342-3.71094 100.342-11.9141L100.342-58.4961C100.342-66.6992 96.2402-70.8008 87.8906-70.8008L22.2168-70.8008C13.916-70.8008 9.76562-66.6992 9.76562-58.4961L9.76562-11.9141C9.76562-3.71094 13.916 0.390625 22.2168 0.390625ZM22.3145-6.68945C18.7988-6.68945 16.7969-8.59375 16.7969-12.2559L16.7969-58.1543C16.7969-61.8652 18.7988-63.7207 22.3145-63.7207L87.793-63.7207C91.3086-63.7207 93.3105-61.8652 93.3105-58.1543L93.3105-12.2559C93.3105-8.59375 91.3086-6.68945 87.793-6.68945Z"/>
<path class="monochrome-1 multicolor-1:tintColor hierarchical-1:secondary SFSymbolsPreviewWireframe" d="M26.6113-40.8203C23.2422-38.7695 26.0742-33.6426 29.7852-35.9863L40.6738-42.8223C43.0664-44.2871 42.9688-47.9492 40.6738-49.4141L29.7852-56.2012C26.0742-58.5938 23.2422-53.4668 26.6113-51.416L35.3516-46.0938ZM43.0176-36.2793C43.0176-34.7656 44.2383-33.4961 45.8008-33.4961L60.3516-33.4961C61.9141-33.4961 63.1348-34.7656 63.1348-36.2793C63.1348-37.8418 61.9141-39.0625 60.3516-39.0625L45.8008-39.0625C44.2383-39.0625 43.0176-37.8418 43.0176-36.2793Z"/>
<path class="monochrome-2 multicolor-2:tintColor hierarchical-2:primary SFSymbolsPreviewWireframe" d="M99.099 18.3447C112.917 18.3447 124.49 6.82128 124.49-7.04592C124.49-20.9619 113.015-32.4365 99.099-32.4365C85.1342-32.4365 73.7084-20.9619 73.7084-7.04592C73.7084 6.96778 85.0854 18.3447 99.099 18.3447Z"/>
<path class="monochrome-3 multicolor-3:systemRedColor hierarchical-3:primary SFSymbolsPreviewWireframe" d="M99.099 12.6807C109.744 12.6807 118.777 3.69628 118.777-7.04592C118.777-17.8369 109.89-26.7236 99.099-26.7236C88.308-26.7236 79.3725-17.8369 79.3725-7.04592C79.3725 3.79398 88.308 12.6807 99.099 12.6807Z"/>
<path class="monochrome-4 multicolor-4:white hierarchical-4:primary SFSymbolsPreviewWireframe" d="M93.8256 2.08498C92.7514 3.15918 91.1401 2.96388 90.1147 1.93848C89.1381 0.913077 88.894-0.698223 89.9682-1.77242L95.3881-7.19242L90.4565-12.124C89.5288-13.1494 89.5288-14.7607 90.4565-15.6396C91.433-16.5674 92.9955-16.6162 94.0209-15.6396L98.9526-10.7568L104.324-16.1279C105.447-17.2509 107.058-17.0068 108.083-15.9814C109.06-15.0049 109.304-13.3935 108.181-12.2705L102.81-6.89942L107.742-1.91892C108.669-0.942423 108.669 0.668977 107.742 1.59668C106.765 2.52438 105.203 2.57328 104.177 1.59668L99.2456-3.33492L93.8256 2.08498Z"/>
</g>
<g id="Ultralight-S" transform="matrix(1 0 0 1 505.702 696)">
<path class="monochrome-0 multicolor-0:tintColor hierarchical-0:secondary SFSymbolsPreviewWireframe" d="M19.7647-1.65282L88.2539-1.65282C94.7871-1.65282 98.2529-5.20946 98.2529-11.6416L98.2529-58.8594C98.2529-65.2915 94.7871-68.8482 88.2539-68.8482L19.7647-68.8482C13.2349-68.8482 9.76562-65.3823 9.76562-58.8594L9.76562-11.6416C9.76562-5.11864 13.2349-1.65282 19.7647-1.65282ZM19.8623-3.87405C14.6665-3.87405 11.9834-6.55031 11.9834-11.7564L11.9834-58.7446C11.9834-63.9541 14.6665-66.6269 19.8623-66.6269L88.1562-66.6269C93.1704-66.6269 96.0351-63.9541 96.0351-58.7446L96.0351-11.7564C96.0351-6.55031 93.1704-3.87405 88.1562-3.87405Z"/>
<path class="monochrome-1 multicolor-1:tintColor hierarchical-1:secondary SFSymbolsPreviewWireframe" d="M24.6133-41.2744C23.1968-40.4497 24.3941-38.456 25.7891-39.3467L37.3589-46.5913C38.4346-47.1933 38.337-48.7212 37.3589-49.3232L25.7891-56.5645C24.3941-57.4131 23.106-55.5556 24.6133-54.6401L35.6694-47.9556ZM39.4302-38.1411C39.4302-37.4902 39.8789-37.0835 40.4878-37.0835L55.0386-37.0835C55.6929-37.0835 56.1417-37.4902 56.1417-38.1411C56.1417-38.75 55.6929-39.1987 55.0386-39.1987L40.4878-39.1987C39.8789-39.1987 39.4302-38.75 39.4302-38.1411Z"/>
<path class="monochrome-2 multicolor-2:tintColor hierarchical-2:primary SFSymbolsPreviewWireframe" d="M97.2163 14.3033C108.9 14.3033 118.565 4.68695 118.565-7.04595C118.565-18.7822 108.953-28.395 97.2163-28.395C85.5219-28.395 75.8671-18.7368 75.8671-7.04595C75.8671 4.69725 85.5185 14.3033 97.2163 14.3033Z"/>
<path class="monochrome-3 multicolor-3:systemRedColor hierarchical-3:primary SFSymbolsPreviewWireframe" d="M97.2163 11.273C107.225 11.273 115.486 3.01515 115.486-7.04595C115.486-17.0649 107.281-25.3159 97.2163-25.3159C87.1972-25.3159 78.9428-17.0195 78.9428-7.04595C78.9428 3.02195 87.1972 11.273 97.2163 11.273Z"/>
<path class="monochrome-4 multicolor-4:white hierarchical-4:primary SFSymbolsPreviewWireframe" d="M90.6713 1.72165C90.142 2.29635 89.1664 2.37355 88.5043 1.71135C87.8457 1.04925 87.9194 0.164552 88.5395-0.455648L95.1401-7.05615L88.8915-13.3047C88.3271-13.9214 88.1909-14.8061 88.8915-15.458C89.5957-16.0679 90.477-15.9805 91.0029-15.458L97.2968-9.21295L103.849-15.81C104.427-16.3882 105.357-16.3711 106.019-15.7998C106.678-15.1865 106.649-14.2109 106.026-13.5874L99.4287-7.03565L105.677-0.829148C106.287-0.215848 106.332 0.623552 105.677 1.32415C105.018 2.02485 104.092 1.93745 103.52 1.32415L97.3173-4.87895L90.6713 1.72165Z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 22 KiB

File diff suppressed because it is too large Load Diff

View File

@ -12,6 +12,7 @@ import CorkShared
import DavidFoundation
import SwiftUI
import UserNotifications
import ButtonKit
@main
struct CorkApp: App
@ -19,7 +20,9 @@ struct CorkApp: App
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate: AppDelegate
@StateObject var brewData: BrewDataStorage = .init()
@StateObject var availableTaps: AvailableTaps = .init()
@StateObject var availableTaps: TapTracker = .init()
@StateObject var cachedDownloadsTracker: CachedPackagesTracker = .init()
@StateObject var topPackagesTracker: TopPackagesTracker = .init()
@ -36,6 +39,7 @@ struct CorkApp: App
@AppStorage("hasFinishedLicensingWorkflow") var hasFinishedLicensingWorkflow: Bool = false
@Environment(\.openWindow) private var openWindow: OpenWindowAction
@AppStorage("showInMenuBar") var showInMenuBar: Bool = false
@AppStorage("areNotificationsEnabled") var areNotificationsEnabled: Bool = false
@ -78,6 +82,7 @@ struct CorkApp: App
})
.environmentObject(appDelegate.appState)
.environmentObject(brewData)
.environmentObject(cachedDownloadsTracker)
.environmentObject(availableTaps)
.environmentObject(updateProgressTracker)
.environmentObject(outdatedPackageTracker)
@ -160,7 +165,7 @@ struct CorkApp: App
{ (completion: NSBackgroundActivityScheduler.CompletionHandler) in
AppConstants.shared.logger.log("Scheduled event fired at \(Date(), privacy: .auto)")
Task(priority: .background)
Task
{ @MainActor in
var updateResult: TerminalOutput = await shell(AppConstants.shared.brewExecutablePath, ["update"])
@ -269,7 +274,7 @@ struct CorkApp: App
}
.onChange(of: areNotificationsEnabled)
{ newValue in // Remove the badge from the app icon if the user turns off notifications, put it back when they turn them back on
Task(priority: .background)
Task
{
await appDelegate.appState.requestNotificationAuthorization()
@ -410,6 +415,15 @@ struct CorkApp: App
}
.windowResizability(.contentSize)
.windowToolbarStyle(.unifiedCompact)
WindowGroup(id: .errorInspectorWindowID, for: String.self)
{ $errorToInspect in
if let errorToInspect
{
ErrorInspector(errorText: errorToInspect)
}
}
.windowToolbarStyle(.unifiedCompact)
Settings
{
@ -425,6 +439,7 @@ struct CorkApp: App
.environmentObject(appDelegate.appState)
.environmentObject(brewData)
.environmentObject(availableTaps)
.environmentObject(cachedDownloadsTracker)
.environmentObject(outdatedPackageTracker)
}
}
@ -520,87 +535,83 @@ struct CorkApp: App
@ViewBuilder
var backupAndRestoreMenuBarSection: some View
{
Button
AsyncButton
{
Task(priority: .userInitiated)
do
{
do
brewfileContents = try await exportBrewfile(appState: appDelegate.appState)
isShowingBrewfileExporter = true
}
catch let brewfileExportError as BrewfileDumpingError
{
AppConstants.shared.logger.error("\(brewfileExportError)")
switch brewfileExportError
{
brewfileContents = try await exportBrewfile(appState: appDelegate.appState)
case .couldNotDetermineWorkingDirectory:
appDelegate.appState.showAlert(errorToShow: .couldNotGetWorkingDirectory)
isShowingBrewfileExporter = true
}
catch let brewfileExportError as BrewfileDumpingError
{
AppConstants.shared.logger.error("\(brewfileExportError)")
case .errorWhileDumpingBrewfile(let error):
appDelegate.appState.showAlert(errorToShow: .couldNotDumpBrewfile(error: error))
switch brewfileExportError
{
case .couldNotDetermineWorkingDirectory:
appDelegate.appState.showAlert(errorToShow: .couldNotGetWorkingDirectory)
case .errorWhileDumpingBrewfile(let error):
appDelegate.appState.showAlert(errorToShow: .couldNotDumpBrewfile(error: error))
case .couldNotReadBrewfile:
appDelegate.appState.showAlert(errorToShow: .couldNotReadBrewfile)
}
case .couldNotReadBrewfile:
appDelegate.appState.showAlert(errorToShow: .couldNotReadBrewfile(error: brewfileExportError.localizedDescription))
}
}
} label: {
Text("navigation.menu.import-export.export-brewfile")
}
.asyncButtonStyle(.plainStyle)
Button
AsyncButton
{
Task(priority: .userInitiated)
do
{
do
let picker: NSOpenPanel = .init()
picker.allowsMultipleSelection = false
picker.canChooseDirectories = false
picker.allowedFileTypes = ["brewbak", ""]
if picker.runModal() == .OK
{
let picker: NSOpenPanel = .init()
picker.allowsMultipleSelection = false
picker.canChooseDirectories = false
picker.allowedFileTypes = ["brewbak", ""]
if picker.runModal() == .OK
guard let brewfileURL = picker.url
else
{
guard let brewfileURL = picker.url
else
{
throw BrewfileReadingError.couldNotGetBrewfileLocation
}
throw BrewfileReadingError.couldNotGetBrewfileLocation
}
AppConstants.shared.logger.debug("\(brewfileURL.path)")
AppConstants.shared.logger.debug("\(brewfileURL.path)")
do
{
try await importBrewfile(from: brewfileURL, appState: appDelegate.appState, brewData: brewData)
}
catch let brewfileImportingError
{
AppConstants.shared.logger.error("\(brewfileImportingError.localizedDescription, privacy: .public)")
do
{
try await importBrewfile(from: brewfileURL, appState: appDelegate.appState, brewData: brewData, cachedPackagesTracker: cachedDownloadsTracker)
}
catch let brewfileImportingError
{
AppConstants.shared.logger.error("\(brewfileImportingError.localizedDescription, privacy: .public)")
appDelegate.appState.showAlert(errorToShow: .malformedBrewfile)
appDelegate.appState.showAlert(errorToShow: .malformedBrewfile)
appDelegate.appState.isShowingBrewfileImportProgress = false
}
appDelegate.appState.showSheet(ofType: .brewfileImport)
}
}
catch let error as BrewfileReadingError
}
catch let error as BrewfileReadingError
{
switch error
{
switch error
{
case .couldNotGetBrewfileLocation:
appDelegate.appState.showAlert(errorToShow: .couldNotGetBrewfileLocation)
case .couldNotGetBrewfileLocation:
appDelegate.appState.showAlert(errorToShow: .couldNotGetBrewfileLocation)
case .couldNotImportFile:
appDelegate.appState.showAlert(errorToShow: .couldNotImportBrewfile)
}
case .couldNotImportFile:
appDelegate.appState.showAlert(errorToShow: .couldNotImportBrewfile)
}
}
} label: {
Text("navigation.menu.import-export.import-brewfile")
}
.asyncButtonStyle(.plainStyle)
}
@ViewBuilder
@ -622,7 +633,7 @@ struct CorkApp: App
{
Button
{
appDelegate.appState.isShowingInstallationSheet.toggle()
appDelegate.appState.showSheet(ofType: .packageInstallation)
} label: {
Text("navigation.menu.packages.install")
}
@ -630,7 +641,7 @@ struct CorkApp: App
Button
{
appDelegate.appState.isShowingAddTapSheet.toggle()
appDelegate.appState.showSheet(ofType: .tapAddition)
} label: {
Text("navigation.menu.packages.add-tap")
}
@ -640,7 +651,7 @@ struct CorkApp: App
Button
{
appDelegate.appState.isShowingUpdateSheet = true
appDelegate.appState.showSheet(ofType: .fullUpdate)
} label: {
Text("navigation.menu.packages.update")
}
@ -664,7 +675,7 @@ struct CorkApp: App
{
Button
{
appDelegate.appState.isShowingMaintenanceSheet.toggle()
appDelegate.appState.showSheet(ofType: .maintenance(fastCacheDeletion: false))
} label: {
Text("navigation.menu.maintenance.perform")
}
@ -672,12 +683,12 @@ struct CorkApp: App
Button
{
appDelegate.appState.isShowingFastCacheDeletionMaintenanceView.toggle()
appDelegate.appState.showSheet(ofType: .maintenance(fastCacheDeletion: true))
} label: {
Text("navigation.menu.maintenance.delete-cached-downloads")
}
.keyboardShortcut("m", modifiers: [.command, .option])
.disabled(appDelegate.appState.cachedDownloadsFolderSize == 0)
.disabled(cachedDownloadsTracker.cachedDownloadsSize == 0)
}
@ViewBuilder
@ -707,6 +718,18 @@ struct CorkApp: App
} label: {
Text("debug.action.licensing")
}
Menu
{
Button
{
openWindow(id: .errorInspectorWindowID, value: PackageLoadingError.packageIsNotAFolder("Hello I am an error", packageURL: .applicationDirectory).localizedDescription)
} label: {
Text("debug.action.show-error-inspector")
}
} label: {
Text("debug.action.ui")
}
}
// MARK: - Functions

View File

@ -10,11 +10,12 @@ import SwiftUI
enum DisplayableAlert: LocalizedError
{
case couldNotLoadAnyPackages(LocalizedError), couldNotLoadCertainPackage(String, URL, failureReason: String)
case couldNotGetContentsOfPackageFolder(String), couldNotLoadAnyPackages(LocalizedError), couldNotLoadCertainPackage(String, URL, failureReason: String)
case licenseCheckingFailedDueToAuthorizationComplexNotBeingEncodedProperly, licenseCheckingFailedDueToNoInternet, licenseCheckingFailedDueToTimeout, licenseCheckingFailedForOtherReason(localizedDescription: String)
case tapLoadingFailedDueToTapParentLocation(localizedDescription: String), tapLoadingFailedDueToTapItself(localizedDescription: String)
case customBrewExcutableGotDeleted
case couldNotFindPackageUUIDInList
case uninstallationNotPossibleDueToDependency(packageThatTheUserIsTryingToUninstall: BrewPackage, offendingDependencyProhibitingUninstallation: String), couldNotApplyTaggedStateToPackages, couldNotClearMetadata, metadataFolderDoesNotExist, couldNotCreateCorkMetadataDirectory, couldNotCreateCorkMetadataFile, installedPackageHasNoVersions(corruptedPackageName: String), installedPackageIsNotAFolder(itemName: String, itemURL: URL), homePathNotSet
case uninstallationNotPossibleDueToDependency(packageThatTheUserIsTryingToUninstall: BrewPackage, offendingDependencyProhibitingUninstallation: String), couldNotApplyTaggedStateToPackages, couldNotClearMetadata, metadataFolderDoesNotExist, couldNotCreateCorkMetadataDirectory, couldNotCreateCorkMetadataFile, installedPackageHasNoVersions(corruptedPackageName: String), installedPackageIsNotAFolder(itemName: String, itemURL: URL), homePathNotSet, numberOfLoadedPackagesDoesNotMatchNumberOfPackageFolders, triedToThreatFolderContainingPackagesAsPackage(packageType: PackageType)
case couldNotObtainNotificationPermissions
case couldNotRemoveTapDueToPackagesFromItStillBeingInstalled(offendingTapProhibitingRemovalOfTap: String)
case couldNotParseTopPackages(error: String)
@ -23,10 +24,13 @@ enum DisplayableAlert: LocalizedError
case couldNotAssociateAnyPackageWithProvidedPackageUUID
case couldNotFindPackageInParentDirectory
case fatalPackageInstallationError(String)
case fatalPackageUninstallationError(packageName: String, errorDetails: String)
case couldNotSynchronizePackages(error: String)
case couldNotDeleteCachedDownloads(error: String)
// MARK: - Brewfile exporting/importing
case couldNotGetWorkingDirectory, couldNotDumpBrewfile(error: String), couldNotReadBrewfile
case couldNotGetWorkingDirectory, couldNotDumpBrewfile(error: String), couldNotReadBrewfile(error: String)
case couldNotGetBrewfileLocation, couldNotImportBrewfile, malformedBrewfile
}

View File

@ -14,6 +14,8 @@ extension DisplayableAlert
{
switch self
{
case .couldNotGetContentsOfPackageFolder:
return String(localized: "alert.could-not-get-contents-of-package-folder.title")
case .couldNotLoadAnyPackages(let error):
return String(localized: "alert.fatal.could-not-load-any-packages-\(error.localizedDescription).title")
case .couldNotLoadCertainPackage(let offendingPackage, _, _):
@ -44,10 +46,12 @@ extension DisplayableAlert
return String(localized: "alert.could-not-create-metadata-file.title")
case .installedPackageHasNoVersions(let corruptedPackageName):
return String(localized: "alert.package-corrupted.title-\(corruptedPackageName)")
case .installedPackageIsNotAFolder(itemName: let itemName, itemURL: _):
case .installedPackageIsNotAFolder(let itemName, _):
return String(localized: "alert.tried-to-load-package-that-is-not-a-folder.title-\(itemName)")
case .homePathNotSet:
return String(localized: "alert.home-not-set.title")
case .numberOfLoadedPackagesDoesNotMatchNumberOfPackageFolders:
return PackageLoadingError.numberOLoadedPackagesDosNotMatchNumberOfPackageFolders.localizedDescription
case .couldNotObtainNotificationPermissions:
return String(localized: "alert.notifications-error-while-obtaining-permissions.title")
case .couldNotRemoveTapDueToPackagesFromItStillBeingInstalled(let offendingTapProhibitingRemovalOfTap):
@ -64,6 +68,8 @@ extension DisplayableAlert
return String(localized: "alert.could-not-find-package-in-parent-directory.title")
case .fatalPackageInstallationError:
return String(localized: "alert.fatal-installation.error")
case .fatalPackageUninstallationError(let packageName, _):
return String(localized: "alert.unable-to-uninstall-\(packageName).title")
case .couldNotSynchronizePackages:
return String(localized: "alert.fatal.could-not-synchronize-packages.title")
case .couldNotGetWorkingDirectory:
@ -78,6 +84,14 @@ extension DisplayableAlert
return String(localized: "alert.could-not-import-brewfile.title")
case .malformedBrewfile:
return String(localized: "alert.malformed-brewfile.title")
case .tapLoadingFailedDueToTapParentLocation:
return String(localized: "alert.tap-loading-failed.tap-parent.title")
case .tapLoadingFailedDueToTapItself:
return String(localized: "alert.tap-loading-failed.tap-itself.title")
case .triedToThreatFolderContainingPackagesAsPackage:
return String(localized: "alert.homebrew-broken.title")
case .couldNotDeleteCachedDownloads:
return String(localized: "alert.cache-deletion-failed.title")
}
}
}

View File

@ -9,10 +9,13 @@ import Foundation
extension DisplayableAlert
{
/// Message in the alert
var recoverySuggestion: String?
{
switch self
{
case .couldNotGetContentsOfPackageFolder(let localizedError):
return String(localized: "alert.could-not-get-contents-of-package-folder.message-\(localizedError)")
case .couldNotLoadAnyPackages:
return String(localized: "alert.restart-or-reinstall")
case .couldNotLoadCertainPackage(_, _, let failureReason):
@ -43,10 +46,12 @@ extension DisplayableAlert
return String(localized: "alert.could-not-create-metadata-directory-or-folder.message")
case .installedPackageHasNoVersions:
return String(localized: "alert.package-corrupted.message")
case .installedPackageIsNotAFolder(itemName: let itemName, _):
case .installedPackageIsNotAFolder(let itemName, _):
return String(localized: "alert.tried-to-load-package-that-is-not-a-folder.message-\(itemName)")
case .homePathNotSet:
return String(localized: "alert.home-not-set.message")
case .numberOfLoadedPackagesDoesNotMatchNumberOfPackageFolders:
return nil
case .couldNotObtainNotificationPermissions:
return String(localized: "alert.notifications-error-while-obtaining-permissions.message")
case .couldNotRemoveTapDueToPackagesFromItStillBeingInstalled(let offendingTapProhibitingRemovalOfTap):
@ -63,20 +68,30 @@ extension DisplayableAlert
return String(localized: "message.try-again-or-restart")
case .fatalPackageInstallationError(let errorDetails):
return errorDetails
case .fatalPackageUninstallationError(_, let errorDetails):
return errorDetails
case .couldNotSynchronizePackages(let error):
return error
case .couldNotGetWorkingDirectory:
return String(localized: "message.try-again-or-restart")
case .couldNotDumpBrewfile(let error):
return String(localized: "message.try-again-or-restart-\(error)")
case .couldNotReadBrewfile:
return String(localized: "message.try-again-or-restart")
case .couldNotReadBrewfile(let error):
return error
case .couldNotGetBrewfileLocation:
return String(localized: "alert.could-not-get-brewfile-location.message")
case .couldNotImportBrewfile:
return String(localized: "alert.could-not-import-brewfile.message")
case .malformedBrewfile:
return String(localized: "alert.malformed-brewfile.message")
case .tapLoadingFailedDueToTapParentLocation(let localizedDescription):
return localizedDescription
case .tapLoadingFailedDueToTapItself(let localizedDescription):
return localizedDescription
case .triedToThreatFolderContainingPackagesAsPackage(let packageType):
return PackageLoadingError.triedToThreatFolderContainingPackagesAsPackage(packageType: packageType).localizedDescription
case .couldNotDeleteCachedDownloads(let associatedError):
return associatedError
}
}
}

View File

@ -7,17 +7,16 @@
import AppIntents
import Charts
import CorkShared
import Foundation
enum PackageType: String, CustomStringConvertible, Plottable, AppEntity, Codable
{
enum PackageType: String, CustomStringConvertible, Plottable, AppEntity, Codable {
case formula
case cask
var description: String
{
switch self
{
/// User-readable description of the package type
var description: String {
switch self {
case .formula:
return String(localized: "package-details.type.formula")
case .cask:
@ -25,12 +24,20 @@ enum PackageType: String, CustomStringConvertible, Plottable, AppEntity, Codable
}
}
/// Parent folder for this package type
var parentFolder: URL {
switch self {
case .formula:
return AppConstants.shared.brewCellarPath
case .cask:
return AppConstants.shared.brewCaskPath
}
}
static let typeDisplayRepresentation: TypeDisplayRepresentation = .init(name: "package-details.type")
var displayRepresentation: DisplayRepresentation
{
switch self
{
var displayRepresentation: DisplayRepresentation {
switch self {
case .formula:
DisplayRepresentation(title: "package-details.type.formula")
case .cask:

View File

@ -0,0 +1,31 @@
//
// Sheets - Main Window.swift
// Cork
//
// Created by David Bureš - P on 19.01.2025.
//
import Foundation
enum DisplayableSheet: Identifiable, Equatable
{
case packageInstallation
case tapAddition
case fullUpdate, partialUpdate
case corruptedPackageFix(corruptedPackage: CorruptedPackage)
case sudoRequiredForPackageRemoval
case maintenance(fastCacheDeletion: Bool)
case brewfileExport, brewfileImport
var id: UUID
{
return .init()
}
}

View File

@ -0,0 +1,30 @@
//
// Cached Download Deletion Error.swift
// Cork
//
// Created by David Bureš - P on 20.01.2025.
//
import Foundation
enum CachedDownloadDeletionError: LocalizedError
{
case couldNotReadContentsOfCachedFormulaeDownloadsFolder(associatedError: String)
case couldNotReadContentsOfCachedCasksDownloadsFolder(associatedError: String)
case couldNotReadContentsOfCachedDownloadsFolder(associatedError: String)
var errorDescription: String?
{
switch self
{
case .couldNotReadContentsOfCachedFormulaeDownloadsFolder:
return String(localized: "error.cache-deletion.could-not-read-contents-of-cached-formulae-downloads-folder")
case .couldNotReadContentsOfCachedCasksDownloadsFolder:
return String(localized: "error.cache-deletion.could-not-read-contents-of-cached-casks-downloads-folder")
case .couldNotReadContentsOfCachedDownloadsFolder:
return String(localized: "error.cache-deletion.could-not-read-contents-of-cached-downloads-folder")
}
}
}

View File

@ -0,0 +1,22 @@
//
// Intent Error.swift
// Cork
//
// Created by David Bureš on 13.11.2024.
//
import Foundation
enum IntentError: LocalizedError
{
case failedWhilePerformingIntent
var errorDescription: String?
{
switch self
{
case .failedWhilePerformingIntent:
return String(localized: "error.intents.general-failure")
}
}
}

View File

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

View File

@ -0,0 +1,22 @@
//
// Package Synchronization Error.swift
// Cork
//
// Created by David Bureš - P on 15.01.2025.
//
import Foundation
enum PackageSynchronizationError: LocalizedError
{
case synchronizationReturnedNil
var errorDescription: String?
{
switch self
{
case .synchronizationReturnedNil:
return String(localized: "error.package-synchronization.returned-nil")
}
}
}

View File

@ -0,0 +1,27 @@
//
// Tap Loading Error.swift
// Cork
//
// Created by David Bureš - P on 04.01.2025.
//
import Foundation
enum TapLoadingError: LocalizedError, Hashable
{
/// Could not read the folder that includes the taps
case couldNotAccessParentTapFolder(errorDetails: String)
/// Could not read a tap itself
case couldNotReadTapFolderContents(errorDetails: String)
var errorDescription: String?
{
switch self {
case .couldNotAccessParentTapFolder(let errorDetails):
return errorDetails
case .couldNotReadTapFolderContents(let errorDetails):
return errorDetails
}
}
}

View File

@ -6,6 +6,7 @@
//
import Foundation
import SwiftUI
extension String
{
@ -13,4 +14,5 @@ extension String
static let previewWindowID: String = "window.package-preview"
static let servicesWindowID: String = "services"
static let aboutWindowID: String = "about"
static let errorInspectorWindowID: String = "error-inspector"
}

View File

@ -0,0 +1,16 @@
//
// URL - Package Name from URL.swift
// Cork
//
// Created by David Bureš - P on 18.01.2025.
//
import Foundation
extension URL
{
func packageNameFromURL() -> String
{
return self.lastPathComponent
}
}

File diff suppressed because it is too large Load Diff

View File

@ -23,11 +23,27 @@ struct GetInstalledCasksIntent: AppIntent
if allowAccessToFile
{
let installedFormulae: Set<BrewPackage> = await loadUpPackages(whatToLoad: .cask, appState: AppState())
let dummyBrewData: BrewDataStorage = await .init()
guard let installedCasks: BrewPackages = await dummyBrewData.loadInstalledPackages(packageTypeToLoad: .cask, appState: AppState()) else
{
throw IntentError.failedWhilePerformingIntent
}
/// Filter out all packages that gave an error
let validInstalledCasks: Set<BrewPackage> = Set(installedCasks.compactMap({ rawResult in
if case let .success(success) = rawResult {
return success
}
else
{
return nil
}
}))
AppConstants.shared.brewCaskPath.stopAccessingSecurityScopedResource()
let minimalPackages: [MinimalHomebrewPackage] = installedFormulae.map
let minimalPackages: [MinimalHomebrewPackage] = validInstalledCasks.map
{ package in
.init(name: package.name, type: .cask, installDate: package.installedOn, installedIntentionally: true)
}

View File

@ -40,11 +40,27 @@ struct GetInstalledFormulaeIntent: AppIntent
if allowAccessToFile
{
let installedFormulae: Set<BrewPackage> = await loadUpPackages(whatToLoad: .formula, appState: AppState())
let dummyBrewData: BrewDataStorage = await .init()
guard let installedFormulae: BrewPackages = await dummyBrewData.loadInstalledPackages(packageTypeToLoad: .formula, appState: AppState()) else
{
throw IntentError.failedWhilePerformingIntent
}
/// Filter out all packages that gave an error
let validInstalledFormulae: Set<BrewPackage> = Set(installedFormulae.compactMap { rawResult in
if case let .success(success) = rawResult {
return success
}
else
{
return nil
}
})
AppConstants.shared.brewCellarPath.stopAccessingSecurityScopedResource()
var minimalPackages: [MinimalHomebrewPackage] = installedFormulae.map
var minimalPackages: [MinimalHomebrewPackage] = validInstalledFormulae.map
{ package in
.init(name: package.name, type: .formula, installedIntentionally: package.installedIntentionally)
}

View File

@ -10,7 +10,7 @@ import CorkShared
enum BrewfileDumpingError: LocalizedError
{
case couldNotDetermineWorkingDirectory, errorWhileDumpingBrewfile(error: String), couldNotReadBrewfile
case couldNotDetermineWorkingDirectory, errorWhileDumpingBrewfile(error: String), couldNotReadBrewfile(error: String)
var errorDescription: String?
{
@ -20,8 +20,8 @@ enum BrewfileDumpingError: LocalizedError
return String(localized: "error.brewfile.export.could-not-determine-working-directory")
case .errorWhileDumpingBrewfile(let error):
return String(localized: "error.brewfile.export.could-not-dump-with-error.\(error)")
case .couldNotReadBrewfile:
return String(localized: "error.brewfile.export.could-not-read-temporary-brewfile")
case .couldNotReadBrewfile(let error):
return error
}
}
}
@ -30,11 +30,11 @@ enum BrewfileDumpingError: LocalizedError
@MainActor
func exportBrewfile(appState: AppState) async throws -> String
{
appState.isShowingBrewfileExportProgress = true
appState.showSheet(ofType: .brewfileExport)
defer
{
appState.isShowingBrewfileExportProgress = false
appState.dismissSheet()
}
let brewfileParentLocation: URL = URL.temporaryDirectory
@ -56,7 +56,7 @@ func exportBrewfile(appState: AppState) async throws -> String
throw BrewfileDumpingError.couldNotDetermineWorkingDirectory
}
if !brewfileDumpingResult.standardError.isEmpty
if brewfileDumpingResult.standardError.contains("(E|e)rror")
{
throw BrewfileDumpingError.errorWhileDumpingBrewfile(error: brewfileDumpingResult.standardError)
}
@ -79,6 +79,6 @@ func exportBrewfile(appState: AppState) async throws -> String
catch let brewfileReadingError
{
AppConstants.shared.logger.error("Error while reading contents of Brewfile: \(brewfileReadingError, privacy: .public)")
throw BrewfileDumpingError.couldNotReadBrewfile
throw BrewfileDumpingError.couldNotReadBrewfile(error: brewfileReadingError.localizedDescription)
}
}

View File

@ -25,9 +25,9 @@ enum BrewfileReadingError: LocalizedError
}
@MainActor
func importBrewfile(from url: URL, appState: AppState, brewData: BrewDataStorage) async throws
func importBrewfile(from url: URL, appState: AppState, brewData: BrewDataStorage, cachedPackagesTracker: CachedPackagesTracker) async throws
{
appState.isShowingBrewfileImportProgress = true
appState.showSheet(ofType: .brewfileImport)
appState.brewfileImportingStage = .importing
@ -44,5 +44,12 @@ func importBrewfile(from url: URL, appState: AppState, brewData: BrewDataStorage
appState.brewfileImportingStage = .finished
await synchronizeInstalledPackages(brewData: brewData)
do
{
try await brewData.synchronizeInstalledPackages(cachedPackagesTracker: cachedPackagesTracker)
}
catch let synchronizationError
{
appState.showAlert(errorToShow: .couldNotSynchronizePackages(error: synchronizationError.localizedDescription))
}
}

View File

@ -0,0 +1,82 @@
//
// Load Cached Package Downloads.swift
// Cork
//
// Created by David Bureš - P on 16.01.2025.
//
import Foundation
import CorkShared
extension CachedPackagesTracker
{
/// Load cached downloads and assign their types
@MainActor
func loadCachedDownloadedPackages(brewData: BrewDataStorage) async
{
AppConstants.shared.logger.info("Will load cached downloaded packages")
self.cachedDownloads = .init()
let smallestDispalyableSize: Int = .init(AppConstants.shared.brewCachedDownloadsPath.directorySize / 50)
var packagesThatAreTooSmallToDisplaySize: Int = 0
guard let cachedDownloadsFolderContents: [URL] = try? FileManager.default.contentsOfDirectory(at: AppConstants.shared.brewCachedDownloadsPath, includingPropertiesForKeys: [.isRegularFileKey], options: [.skipsHiddenFiles])
else
{
return
}
let usableCachedDownloads: [URL] = cachedDownloadsFolderContents.filter { $0.pathExtension != "json" }
for usableCachedDownload in usableCachedDownloads
{
guard var itemName: String = try? usableCachedDownload.lastPathComponent.regexMatch("(?<=--)(.*?)(?=\\.)")
else
{
return
}
AppConstants.shared.logger.debug("Temp item name: \(itemName, privacy: .public)")
if itemName.contains("--")
{
do
{
itemName = try itemName.regexMatch(".*?(?=--)")
}
catch {}
}
guard let itemAttributes = try? FileManager.default.attributesOfItem(atPath: usableCachedDownload.path)
else
{
return
}
guard let itemSize = itemAttributes[.size] as? Int
else
{
return
}
if itemSize < smallestDispalyableSize
{
packagesThatAreTooSmallToDisplaySize = packagesThatAreTooSmallToDisplaySize + itemSize
}
else
{
cachedDownloads.append(CachedDownload(packageName: itemName, sizeInBytes: itemSize))
}
AppConstants.shared.logger.debug("Others size: \(packagesThatAreTooSmallToDisplaySize, privacy: .public)")
}
cachedDownloads = cachedDownloads.sorted(by: { $0.sizeInBytes < $1.sizeInBytes })
cachedDownloads.append(.init(packageName: String(localized: "start-page.cached-downloads.graph.other-smaller-packages"), sizeInBytes: packagesThatAreTooSmallToDisplaySize, packageType: .other))
self.assignPackageTypeToCachedDownloads(brewData: brewData)
}
}

View File

@ -9,33 +9,7 @@ import Foundation
import SwiftUI
import CorkShared
enum PackageLoadingError: LocalizedError
{
case failedWhileLoadingPackages(failureReason: String?), failedWhileLoadingCertainPackage(String, URL, failureReason: String), packageDoesNotHaveAnyVersionsInstalled(String), packageIsNotAFolder(String, URL)
var errorDescription: String?
{
switch self
{
case .failedWhileLoadingPackages(let failureReason):
if let failureReason
{
return String(localized: "error.package-loading.could-not-load-packages.\(failureReason)")
}
else
{
return String(localized: "error.package-loading.could-not-load-packages")
}
case .failedWhileLoadingCertainPackage(let string, let uRL, let failureReason):
return String(localized: "error.package-loading.could-not-load-\(string)-at-\(uRL.absoluteString)-because-\(failureReason)", comment: "Couldn't load package (package name) at (package URL) because (failure reason)")
case .packageDoesNotHaveAnyVersionsInstalled(let string):
return String(localized: "error.package-loading.\(string)-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")
}
}
}
/*
func getContentsOfFolder(targetFolder: URL) async throws -> Set<BrewPackage>
{
do
@ -112,7 +86,8 @@ func getContentsOfFolder(targetFolder: URL) async throws -> Set<BrewPackage>
throw error
}
}
*/
// MARK: - Sub-functions
private extension URL
@ -138,114 +113,6 @@ private extension URL
return items
}
/// This function checks whether the package was installed intentionally.
/// - For Formulae, this info gets read from the install receipt
/// - Casks are always instaled intentionally
/// - Parameter versionURLs: All available versions for this package. Some packages have multiple versions installed at a time (for example, the package `xz` might have versions 1.2 and 1.3 installed at once)
/// - Returns: Indication whether this package was installed intentionally or not
func checkIfPackageWasInstalledIntentionally(_ versionURLs: [URL]) async throws -> Bool
{
guard let localPackagePath = versionURLs.first
else
{
throw PackageLoadingError.failedWhileLoadingCertainPackage(lastPathComponent, self, failureReason: String(localized: "error.package-loading.could-not-load-version-to-check-from-available-versions"))
}
guard localPackagePath.lastPathComponent != "Cellar"
else
{
AppConstants.shared.logger.error("The last path component of the requested URL is the package container folder itself - perhaps a misconfigured package folder? Tried to load URL \(localPackagePath)")
throw PackageLoadingError.failedWhileLoadingPackages(failureReason: String(localized: "error.package-loading.last-path-component-of-checked-package-url-is-folder-containing-packages-itself.formulae"))
}
guard localPackagePath.lastPathComponent != "Caskroom"
else
{
AppConstants.shared.logger.error("The last path component of the requested URL is the package container folder itself - perhaps a misconfigured package folder? Tried to load URL \(localPackagePath)")
throw PackageLoadingError.failedWhileLoadingPackages(failureReason: String(localized: "error.package-loading.last-path-component-of-checked-package-url-is-folder-containing-packages-itself.casks"))
}
if path.contains("Cellar")
{
let localPackageInfoJSONPath: URL = localPackagePath.appendingPathComponent("INSTALL_RECEIPT.json", conformingTo: .json)
if FileManager.default.fileExists(atPath: localPackageInfoJSONPath.path)
{
struct InstallRecepitParser: Codable
{
let installedOnRequest: Bool
}
let decoder: JSONDecoder = {
let decoder: JSONDecoder = .init()
decoder.keyDecodingStrategy = .convertFromSnakeCase
return decoder
}()
do
{
let installReceiptContents: Data = try .init(contentsOf: localPackageInfoJSONPath)
do
{
return try decoder.decode(InstallRecepitParser.self, from: installReceiptContents).installedOnRequest
}
catch let installReceiptParsingError
{
AppConstants.shared.logger.error("Failed to decode install receipt for package \(self.lastPathComponent, privacy: .public) with error \(installReceiptParsingError.localizedDescription, privacy: .public)")
throw PackageLoadingError.failedWhileLoadingCertainPackage(self.lastPathComponent, self, failureReason: String(localized: "error.package-loading.could-not-decode-installa-receipt-\(installReceiptParsingError.localizedDescription)"))
}
}
catch let installReceiptLoadingError
{
AppConstants.shared.logger.error("Failed to load contents of install receipt for package \(self.lastPathComponent, privacy: .public) with error \(installReceiptLoadingError.localizedDescription, privacy: .public)")
throw PackageLoadingError.failedWhileLoadingCertainPackage(self.lastPathComponent, self, failureReason: String(localized: "error.package-loading.could-not-convert-contents-of-install-receipt-to-data-\(installReceiptLoadingError.localizedDescription)"))
}
}
else
{ /// There's no install receipt for this package - silently fail and return that the packagw was not installed intentionally
// TODO: Add a setting like "Strictly check for errors" that would instead throw an error here
AppConstants.shared.logger.error("There appears to be no install receipt for package \(localPackageInfoJSONPath.lastPathComponent, privacy: .public)")
let shouldStrictlyCheckForHomebrewErrors: Bool = UserDefaults.standard.bool(forKey: "strictlyCheckForHomebrewErrors")
if shouldStrictlyCheckForHomebrewErrors
{
throw PackageLoadingError.failedWhileLoadingCertainPackage(lastPathComponent, self, failureReason: String(localized: "error.package-loading.missing-install-receipt"))
}
else
{
return false
}
}
}
else if path.contains("Caskroom")
{
return true
}
else
{
throw PackageLoadingError.failedWhileLoadingCertainPackage(lastPathComponent, self, failureReason: String(localized: "error.package-loading.unexpected-folder-name"))
}
}
/// Determine a package's type type from its URL
var packageType: PackageType
{
if path.contains("Cellar")
{
return .formula
}
else
{
return .cask
}
}
/// Get URLs to a package's versions
var packageVersionURLs: [URL]?
{
@ -288,25 +155,23 @@ extension [URL]
// MARK: - Getting list of URLs in folder
func getContentsOfFolder(targetFolder: URL, options: FileManager.DirectoryEnumerationOptions? = nil) -> [URL]
func getContentsOfFolder(targetFolder: URL, options: FileManager.DirectoryEnumerationOptions? = nil) throws -> [URL]
{
var contentsOfFolder: [URL] = .init()
do
{
if let options
{
contentsOfFolder = try FileManager.default.contentsOfDirectory(at: targetFolder, includingPropertiesForKeys: nil, options: options)
return try FileManager.default.contentsOfDirectory(at: targetFolder, includingPropertiesForKeys: nil, options: options)
}
else
{
contentsOfFolder = try FileManager.default.contentsOfDirectory(at: targetFolder, includingPropertiesForKeys: nil)
return try FileManager.default.contentsOfDirectory(at: targetFolder, includingPropertiesForKeys: nil)
}
}
catch let folderReadingError as NSError
catch let folderReadingError
{
AppConstants.shared.logger.error("\(folderReadingError.localizedDescription)")
throw folderReadingError
}
return contentsOfFolder
}

View File

@ -7,39 +7,3 @@
import Foundation
import CorkShared
@MainActor
func applyUninstallationSpinner(to package: BrewPackage, brewData: BrewDataStorage)
{
AppConstants.shared.logger.debug("""
Brew data:
Installed Formulae: \(brewData.installedFormulae)
Installed Casks: \(brewData.installedCasks)
""")
AppConstants.shared.logger.debug("Will try to apply uninstallation spinner to package \(package.name)")
if package.type == .cask
{
brewData.installedFormulae = Set(brewData.installedFormulae.map
{ formula in
var copyFormula: BrewPackage = formula
if copyFormula.name == package.name
{
copyFormula.changeBeingModifiedStatus()
}
return copyFormula
})
}
else
{
brewData.installedFormulae = Set(brewData.installedCasks.map
{ cask in
var copyCask: BrewPackage = cask
if copyCask.name == package.name
{
copyCask.changeBeingModifiedStatus()
}
return copyCask
})
}
}

View File

@ -5,54 +5,37 @@
// Created by David Bureš on 05.02.2023.
//
import CorkShared
import Foundation
import SwiftUI
import CorkShared
extension BrewDataStorage
{
@MainActor
func uninstallSelectedPackage(
package: BrewPackage,
cachedPackagesTracker: CachedPackagesTracker,
appState: AppState,
outdatedPackageTracker: OutdatedPackageTracker,
shouldRemoveAllAssociatedFiles: Bool,
shouldApplyUninstallSpinnerToRelevantItemInSidebar: Bool = false
shouldApplyUninstallSpinnerToRelevantItemInSidebar _: Bool = false
) async throws
{
/// Store the old navigation selection to see if it got updated in the middle of switching
let oldNavigationSelectionId: UUID? = appState.navigationTargetId
if shouldApplyUninstallSpinnerToRelevantItemInSidebar
{
if package.type == .formula
{
installedFormulae = Set(installedFormulae.map
{ formula in
var copyFormula: BrewPackage = formula
if copyFormula.name == package.name
{
copyFormula.changeBeingModifiedStatus()
}
return copyFormula
})
}
else
{
installedCasks = Set(installedCasks.map
{ cask in
var copyCask: BrewPackage = cask
if copyCask.name == package.name
{
copyCask.changeBeingModifiedStatus()
}
return copyCask
})
}
self.updatePackageInPlace(package)
{ package in
package.changeBeingModifiedStatus()
}
else
defer
{
appState.isShowingUninstallationProgressView = true
AppConstants.shared.logger.debug("Would reset state now")
self.updatePackageInPlace(package)
{ package in
package.isBeingModified = false
}
}
AppConstants.shared.logger.info("Will try to remove package \(package.name, privacy: .auto)")
@ -73,9 +56,6 @@ extension BrewDataStorage
{
AppConstants.shared.logger.warning("Could not uninstall this package because it's a dependency")
/// If the uninstallation failed, change the status back to "not being modified"
resetPackageState(package: package)
do
{
let dependencyName: String = try uninstallCommandOutput.standardError.regexMatch("(?<=required by ).*?(?=, which)")
@ -97,36 +77,38 @@ extension BrewDataStorage
AppConstants.shared.logger.error("Could not uninstall this package because sudo is required")
appState.packageTryingToBeUninstalledWithSudo = package
appState.isShowingSudoRequiredForUninstallSheet = true
resetPackageState(package: package)
appState.showSheet(ofType: .sudoRequiredForPackageRemoval)
}
else
{
AppConstants.shared.logger.info("Uninstalling can proceed")
switch package.type
do
{
case .formula:
withAnimation
try await self.synchronizeInstalledPackages(cachedPackagesTracker: cachedPackagesTracker)
if !uninstallCommandOutput.standardError.isEmpty && uninstallCommandOutput.standardError.contains("Error:")
{
self.removeFormulaFromTracker(withName: package.name)
AppConstants.shared.logger.error("There was a serious uninstall error: \(uninstallCommandOutput.standardError)")
appState.showAlert(errorToShow: .fatalPackageUninstallationError(packageName: package.name, errorDetails: uninstallCommandOutput.standardError))
}
case .cask:
withAnimation
else
{
self.removeCaskFromTracker(withName: package.name)
AppConstants.shared.logger.info("Uninstalling can proceed")
if appState.navigationTargetId != nil
{
/// Switch to the status page only if the user didn't open another details window in the middle of the uninstall process
if oldNavigationSelectionId == appState.navigationTargetId
{
appState.navigationTargetId = nil
}
}
}
}
if appState.navigationTargetId != nil
catch let synchronizationError
{
/// Switch to the status page only if the user didn't open another details window in the middle of the uninstall process
if oldNavigationSelectionId == appState.navigationTargetId
{
appState.navigationTargetId = nil
}
appState.showAlert(errorToShow: .couldNotSynchronizePackages(error: synchronizationError.localizedDescription))
}
}
@ -135,41 +117,9 @@ extension BrewDataStorage
AppConstants.shared.logger.info("Package uninstallation process output:\nStandard output: \(uninstallCommandOutput.standardOutput, privacy: .public)\nStandard error: \(uninstallCommandOutput.standardError, privacy: .public)")
/// If the user removed a package that was outdated, remove it from the outdated package tracker
Task
if let index = outdatedPackageTracker.displayableOutdatedPackages.firstIndex(where: { $0.package.name == package.name })
{
if let index = outdatedPackageTracker.displayableOutdatedPackages.firstIndex(where: { $0.package.name == package.name })
{
outdatedPackageTracker.outdatedPackages.remove(at: index)
}
}
}
@MainActor
private func resetPackageState(package: BrewPackage)
{
if package.type == .formula
{
installedFormulae = Set(installedFormulae.map
{ formula in
var copyFormula: BrewPackage = formula
if copyFormula.name == package.name, copyFormula.isBeingModified == true
{
copyFormula.changeBeingModifiedStatus()
}
return copyFormula
})
}
else
{
installedCasks = Set(installedCasks.map
{ cask in
var copyCask: BrewPackage = cask
if copyCask.name == package.name, copyCask.isBeingModified == true
{
copyCask.changeBeingModifiedStatus()
}
return copyCask
})
outdatedPackageTracker.outdatedPackages.remove(at: index)
}
}
}

View File

@ -5,55 +5,34 @@
// Created by David Bureš on 23.02.2023.
//
import CorkShared
import Foundation
import SwiftUI
/// What this function does:
/// **Background**
/// When a package is installed or uninstalled, its dependencies don't show up in the package list, because there's no system for getting them. I needed a way for all new packages to show up/disappear even when they were not installed manually
/// **Motivation**
/// Because I can't fucking figure out why I can't subtract one array from another, I had to come up with this contrived mess
/// **Process**
/// The function works like this
/// 1. Load up installed *formulae* and *casks* after an install/uninstall process has finished
/// 2. Sort the array of both the old and new packages by install date
/// 3. Compare them to see which one is newer:
/// - If the new array is **BIGGER**, it means that packages have been *uninstalled* and we have to be **REMOVING** packages from the old array
/// - If the new array is **SMALLER**, it means that packages have been *installed* and we have to be **ADDING** packages to the old array
/// 4. Depending on what turns out to be the case:
/// - In case we need to be adding packages:
/// 1. Take the length of the old array. This is where all the old packages are. Save this length into a variable *x*
/// 2. From the new array, remove the first *x* elements, so we are only left with the new packages.
/// - In case we need to be removing packages:
/// 1. Copy the old array into another array, called *mutable*, where we will do all the operations
/// 2. In the first for loop, check each package in the old array against each package in the new array. If their names match, it means that this package was not removed, so remove it from the *mutable* array
/// 3. When this process finished, we are left with only packages that are in the old array, but not in the new array: In other words, all the packages that have been removed
/// 4. In the second for loop, check each package in the old array against each package in the final *mutable* array. If the names of packages match, remove them; if they match, it means that this package has been removed
@MainActor
func synchronizeInstalledPackages(brewData: BrewDataStorage) async
extension BrewDataStorage
{
let dummyAppState: AppState = .init()
dummyAppState.isLoadingFormulae = false
dummyAppState.isLoadingCasks = false
/// These have to use this dummy AppState, which forces them to not activate the "loading" animation. We don't want the entire thing to re-draw
let newFormulae: Set<BrewPackage> = await loadUpPackages(whatToLoad: .formula, appState: dummyAppState)
let newCasks: Set<BrewPackage> = await loadUpPackages(whatToLoad: .cask, appState: dummyAppState)
if newFormulae.count != brewData.installedFormulae.count
/// Synchronizes installed packages and cached downloads
func synchronizeInstalledPackages(cachedPackagesTracker: CachedPackagesTracker) async throws(PackageSynchronizationError)
{
AppConstants.shared.logger.debug("Will start synchronization process")
async let updatedFormulaeTracker: BrewPackages? = await self.loadInstalledPackages(packageTypeToLoad: .formula, appState: AppState())
async let updatedCasksTracker: BrewPackages? = await self.loadInstalledPackages(packageTypeToLoad: .cask, appState: AppState())
print("Updated formulae: \(String(describing: await updatedFormulaeTracker))")
print("Updated casks: \(String(describing: await updatedCasksTracker))")
guard let safeUpdatedFormulaeTracker = await updatedFormulaeTracker, let safeUpdatedCasksTracker = await updatedCasksTracker else
{
throw .synchronizationReturnedNil
}
withAnimation
{
brewData.installedFormulae = newFormulae
}
}
if newCasks.count != brewData.installedCasks.count
{
withAnimation
{
brewData.installedCasks = newCasks
self.installedFormulae = safeUpdatedFormulaeTracker
self.installedCasks = safeUpdatedCasksTracker
}
await cachedPackagesTracker.loadCachedDownloadedPackages(brewData: self)
}
}

View File

@ -8,104 +8,131 @@
import Foundation
import CorkShared
@MainActor
func loadUpTappedTaps() async -> [BrewTap]
extension TapTracker
{
var finalAvailableTaps: [BrewTap] = .init()
let contentsOfTapFolder: [URL] = getContentsOfFolder(targetFolder: AppConstants.shared.tapPath, options: .skipsHiddenFiles)
AppConstants.shared.logger.debug("Contents of tap folder: \(contentsOfTapFolder)")
for tapRepoParentURL in contentsOfTapFolder
@MainActor
func loadUpTappedTaps() async throws(TapLoadingError) -> [BrewTap]
{
AppConstants.shared.logger.debug("Tap repo: \(tapRepoParentURL)")
var finalAvailableTaps: [BrewTap] = .init()
let contentsOfTapRepoParent: [URL] = getContentsOfFolder(targetFolder: tapRepoParentURL, options: .skipsHiddenFiles)
for repoURL in contentsOfTapRepoParent
do
{
let repoParentComponents: [String] = repoURL.pathComponents
let contentsOfTapFolder: [URL] = try getContentsOfFolder(targetFolder: AppConstants.shared.tapPath, options: .skipsHiddenFiles)
let repoParentName: String = repoParentComponents.penultimate()!
AppConstants.shared.logger.debug("Contents of tap folder: \(contentsOfTapFolder)")
let repoNameRaw: String = repoParentComponents.last!
let repoName: String = .init(repoNameRaw.dropFirst(9))
let fullTapName: String = "\(repoParentName)/\(repoName)"
AppConstants.shared.logger.info("Full tap name: \(fullTapName)")
finalAvailableTaps.append(BrewTap(name: fullTapName))
}
}
let nonLocalBasicTaps: [BrewTap] = await withTaskGroup(of: BrewTap?.self)
{ taskGroup in
if finalAvailableTaps.filter({ $0.name == "homebrew/core" }).isEmpty
{
AppConstants.shared.logger.warning("Couldn't find homebrew/core in local taps")
taskGroup.addTask
for tapRepoParentURL in contentsOfTapFolder
{
let isCoreAdded: Bool = await checkIfTapIsAdded(tapToCheck: "homebrew/core")
if isCoreAdded
AppConstants.shared.logger.debug("Tap repo: \(tapRepoParentURL)")
do
{
AppConstants.shared.logger.info("homebrew/core is added, but not in local taps")
return BrewTap(name: "homebrew/core")
let contentsOfTapRepoParent: [URL] = try getContentsOfFolder(targetFolder: tapRepoParentURL, options: .skipsHiddenFiles)
for repoURL in contentsOfTapRepoParent
{
let repoParentComponents: [String] = repoURL.pathComponents
let repoParentName: String = repoParentComponents.penultimate()!
let repoNameRaw: String = repoParentComponents.last!
let repoName: String = .init(repoNameRaw.dropFirst(9))
let fullTapName: String = "\(repoParentName)/\(repoName)"
AppConstants.shared.logger.info("Full tap name: \(fullTapName)")
finalAvailableTaps.append(BrewTap(name: fullTapName))
}
}
catch let tapFolderReadingError
{
throw TapLoadingError.couldNotReadTapFolderContents(errorDetails: tapFolderReadingError.localizedDescription)
}
}
let nonLocalBasicTaps: [BrewTap] = await withTaskGroup(of: BrewTap?.self)
{ taskGroup in
if finalAvailableTaps.filter({ $0.name == "homebrew/core" }).isEmpty
{
AppConstants.shared.logger.warning("Couldn't find homebrew/core in local taps")
taskGroup.addTask
{
let isCoreAdded: Bool = await self.checkIfTapIsAdded(tapToCheck: "homebrew/core")
if isCoreAdded
{
AppConstants.shared.logger.info("homebrew/core is added, but not in local taps")
return BrewTap(name: "homebrew/core")
}
else
{
AppConstants.shared.logger.warning("homebrew/core is not added and not in local taps")
return nil
}
}
}
else
{
AppConstants.shared.logger.warning("homebrew/core is not added and not in local taps")
return nil
AppConstants.shared.logger.info("Found homebrew/core in local taps")
}
}
}
else
{
AppConstants.shared.logger.info("Found homebrew/core in local taps")
}
if finalAvailableTaps.filter({ $0.name == "homebrew/cask" }).isEmpty
{
AppConstants.shared.logger.warning("Couldn't find homebrew/cask in local taps")
taskGroup.addTask
{
let isCaskAdded: Bool = await checkIfTapIsAdded(tapToCheck: "homebrew/cask")
if isCaskAdded
if finalAvailableTaps.filter({ $0.name == "homebrew/cask" }).isEmpty
{
return BrewTap(name: "homebrew/cask")
AppConstants.shared.logger.warning("Couldn't find homebrew/cask in local taps")
taskGroup.addTask
{
let isCaskAdded: Bool = await self.checkIfTapIsAdded(tapToCheck: "homebrew/cask")
if isCaskAdded
{
return BrewTap(name: "homebrew/cask")
}
else
{
AppConstants.shared.logger.warning("homebrew/cask is not added and not in local taps")
return nil
}
}
}
else
{
AppConstants.shared.logger.warning("homebrew/cask is not added and not in local taps")
return nil
AppConstants.shared.logger.info("Found homebrew/cask in local taps")
}
var nonLocalBasicTapsInternal: [BrewTap] = .init()
for await tap in taskGroup
{
if let tap = tap
{
nonLocalBasicTapsInternal.append(tap)
}
}
return nonLocalBasicTapsInternal
}
}
else
{
AppConstants.shared.logger.info("Found homebrew/cask in local taps")
}
var nonLocalBasicTapsInternal: [BrewTap] = .init()
finalAvailableTaps.append(contentsOf: nonLocalBasicTaps)
for await tap in taskGroup
return finalAvailableTaps
}
catch let tapFolderReadingError
{
if let tap = tap
let shouldStrictlyCheckForHomebrewErrors: Bool = UserDefaults.standard.bool(forKey: "strictlyCheckForHomebrewErrors")
if shouldStrictlyCheckForHomebrewErrors
{
nonLocalBasicTapsInternal.append(tap)
throw TapLoadingError.couldNotAccessParentTapFolder(errorDetails: tapFolderReadingError.localizedDescription)
}
else
{
return [.init(name: "homebrew/core"), .init(name: "homebrew/cask")]
}
}
return nonLocalBasicTapsInternal
}
finalAvailableTaps.append(contentsOf: nonLocalBasicTaps)
return finalAvailableTaps
}
private func checkIfTapIsAdded(tapToCheck _: String) async -> Bool
{
return true
private func checkIfTapIsAdded(tapToCheck _: String) async -> Bool
{
return true
}
}

View File

@ -8,65 +8,103 @@
import Foundation
import CorkShared
func deleteCachedDownloads()
func deleteCachedDownloads() throws(CachedDownloadDeletionError)
{
let shouldStrictlyCheckForHomebrewErrors: Bool = UserDefaults.standard.bool(forKey: "strictlyCheckForHomebrewErrors")
/// This folder has the symlinks, so we have do **delete ONLY THE SYMLINKS**
for url in getContentsOfFolder(targetFolder: AppConstants.shared.brewCachedFormulaeDownloadsPath)
do
{
if let isSymlink = url.isSymlink()
for url in try getContentsOfFolder(targetFolder: AppConstants.shared.brewCachedFormulaeDownloadsPath)
{
if isSymlink
if let isSymlink = url.isSymlink()
{
try? FileManager.default.removeItem(at: url)
if isSymlink
{
try? FileManager.default.removeItem(at: url)
}
else
{
AppConstants.shared.logger.info("Ignoring cached download at location \(url, privacy: .auto)")
}
}
else
{
AppConstants.shared.logger.info("Ignoring cached download at location \(url, privacy: .auto)")
AppConstants.shared.logger.warning("Could not check symlink status of \(url)")
}
}
else
}
catch let brewCachedFormulaeDownloadsFolderReadingError
{
AppConstants.shared.logger.error("Failed while deleting cached downloads (brewCachedFormulaeDownloadsPath): \(brewCachedFormulaeDownloadsFolderReadingError)")
if shouldStrictlyCheckForHomebrewErrors
{
AppConstants.shared.logger.warning("Could not check symlink status of \(url)")
throw .couldNotReadContentsOfCachedFormulaeDownloadsFolder(associatedError: brewCachedFormulaeDownloadsFolderReadingError.localizedDescription)
}
}
/// This folder has the symlinks, so we have to **delete ONLY THE SYMLINKS**
for url in getContentsOfFolder(targetFolder: AppConstants.shared.brewCachedCasksDownloadsPath)
do
{
if let isSymlink = url.isSymlink()
for url in try getContentsOfFolder(targetFolder: AppConstants.shared.brewCachedCasksDownloadsPath)
{
if isSymlink
if let isSymlink = url.isSymlink()
{
try? FileManager.default.removeItem(at: url)
if isSymlink
{
try? FileManager.default.removeItem(at: url)
}
else
{
AppConstants.shared.logger.info("Ignoring cached download at location \(url, privacy: .auto)")
}
}
else
{
AppConstants.shared.logger.info("Ignoring cached download at location \(url, privacy: .auto)")
AppConstants.shared.logger.warning("Could not check symlink status of \(url)")
}
}
else
}
catch let brewCachedCasksDownloadsFolderReadingError
{
AppConstants.shared.logger.error("Failed while deleting cached downloads (brewCachedCasksDownloadsPath): \(brewCachedCasksDownloadsFolderReadingError)")
if shouldStrictlyCheckForHomebrewErrors
{
AppConstants.shared.logger.warning("Could not check symlink status of \(url)")
throw .couldNotReadContentsOfCachedDownloadsFolder(associatedError: brewCachedCasksDownloadsFolderReadingError.localizedDescription)
}
}
/// This folder has the downloads themselves, so we have do **DELETE EVERYTHING THAT IS NOT A SYMLINK**
for url in getContentsOfFolder(targetFolder: AppConstants.shared.brewCachedDownloadsPath)
do
{
if let isSymlink = url.isSymlink()
for url in try getContentsOfFolder(targetFolder: AppConstants.shared.brewCachedDownloadsPath)
{
if isSymlink
if let isSymlink = url.isSymlink()
{
AppConstants.shared.logger.info("Ignoring cached download at location \(url, privacy: .auto)")
if isSymlink
{
AppConstants.shared.logger.info("Ignoring cached download at location \(url, privacy: .auto)")
}
else
{
try? FileManager.default.removeItem(at: url)
}
}
else
{
try? FileManager.default.removeItem(at: url)
AppConstants.shared.logger.warning("Could not check symlink status of \(url)")
}
}
else
}
catch let brewCachedDownloadsFolderReadingError
{
AppConstants.shared.logger.error("Failed while deleting cached downloads (brewCachedDownloadsPath): \(brewCachedDownloadsFolderReadingError)")
if shouldStrictlyCheckForHomebrewErrors
{
AppConstants.shared.logger.warning("Could not check symlink status of \(url)")
throw .couldNotReadContentsOfCachedDownloadsFolder(associatedError: brewCachedDownloadsFolderReadingError.localizedDescription)
}
}
}

View File

@ -5,9 +5,9 @@
// Created by David Bureš on 21.06.2024.
//
import CorkShared
import Foundation
import SwiftUI
import CorkShared
enum OutdatedPackageRetrievalError: LocalizedError
{
@ -57,17 +57,33 @@ extension OutdatedPackageTracker
let formulae: [Formulae]
let casks: [Casks]
}
func getOutdatedPackages(brewData: BrewDataStorage) async throws
{
/// ``Set<OutdatedPackage>`` that holds packages whose updates are managed by Homebrew
async let outdatedPackagesNonGreedy: Set<OutdatedPackage> = try await getOutdatedPackagesInternal(brewData: brewData, forUpdatingType: .homebrew)
/// ``Set<OutdatedPackage>`` that holds packages whose updates are managed by Homebrew, plus those that are not
async let outdatedPackagesGreedy: Set<OutdatedPackage> = try await getOutdatedPackagesInternal(brewData: brewData, forUpdatingType: .selfUpdating)
print("Contents of non-greedy update checker: \(try await outdatedPackagesNonGreedy.map(\.package.name)), \(try await outdatedPackagesNonGreedy.count)")
print("Contents of greedy update checker: \(try await outdatedPackagesGreedy.map(\.package.name)), \(try await outdatedPackagesGreedy.count)")
/// This includes only those packages that are greedy
let difference: Set<OutdatedPackage> = try await outdatedPackagesGreedy.subtracting(outdatedPackagesNonGreedy)
self.outdatedPackages = try await outdatedPackagesNonGreedy.union(difference)
}
/// Load outdated packages into the outdated package tracker
func getOutdatedPackages(brewData: BrewDataStorage) async throws
private func getOutdatedPackagesInternal(brewData: BrewDataStorage, forUpdatingType updatingType: OutdatedPackage.PackageUpdatingType) async throws -> Set<OutdatedPackage>
{
// First, we have to pull the newest updates
await shell(AppConstants.shared.brewExecutablePath, ["update"])
// Then we can get the updating under way
let rawOutput: TerminalOutput = await shell(AppConstants.shared.brewExecutablePath, ["outdated", "--json=v2"])
print("Outdated package function oputput: \(rawOutput)")
/// Introduces an empty argument in case the updating is non-greedy
let rawOutput: TerminalOutput = await shell(AppConstants.shared.brewExecutablePath, ["outdated", updatingType.argument, "--json=v2"])
// MARK: - Error checking
@ -105,10 +121,10 @@ extension OutdatedPackageTracker
// MARK: - Outdated package matching
async let finalOutdatedFormulae: Set<OutdatedPackage> = await getOutdatedFormulae(from: rawDecodedOutdatedPackages.formulae, brewData: brewData)
async let finalOutdatedCasks: Set<OutdatedPackage> = await getOutdatedCasks(from: rawDecodedOutdatedPackages.casks, brewData: brewData)
async let finalOutdatedFormulae: Set<OutdatedPackage> = await getOutdatedFormulae(from: rawDecodedOutdatedPackages.formulae, brewData: brewData, forUpdatingType: updatingType)
async let finalOutdatedCasks: Set<OutdatedPackage> = await getOutdatedCasks(from: rawDecodedOutdatedPackages.casks, brewData: brewData, forUpdatingType: updatingType)
outdatedPackages = await finalOutdatedFormulae.union(finalOutdatedCasks)
return await finalOutdatedFormulae.union(finalOutdatedCasks)
}
catch let decodingError
{
@ -119,18 +135,19 @@ extension OutdatedPackageTracker
// MARK: - Helper functions
private func getOutdatedFormulae(from intermediaryArray: [OutdatedPackageCommandOutput.Formulae], brewData: BrewDataStorage) async -> Set<OutdatedPackage>
private func getOutdatedFormulae(from intermediaryArray: [OutdatedPackageCommandOutput.Formulae], brewData: BrewDataStorage, forUpdatingType updatingType: OutdatedPackage.PackageUpdatingType) async -> Set<OutdatedPackage>
{
var finalOutdatedFormulaTracker: Set<OutdatedPackage> = .init()
for outdatedFormula in intermediaryArray
{
if let foundOutdatedFormula = brewData.installedFormulae.first(where: { $0.name == outdatedFormula.name })
if let foundOutdatedFormula = brewData.successfullyLoadedFormulae.first(where: { $0.name == outdatedFormula.name })
{
finalOutdatedFormulaTracker.insert(.init(
package: foundOutdatedFormula,
installedVersions: outdatedFormula.installedVersions,
newerVersion: outdatedFormula.currentVersion
newerVersion: outdatedFormula.currentVersion,
updatingManagedBy: updatingType
)
)
}
@ -139,18 +156,19 @@ extension OutdatedPackageTracker
return finalOutdatedFormulaTracker
}
private func getOutdatedCasks(from intermediaryArray: [OutdatedPackageCommandOutput.Casks], brewData: BrewDataStorage) async -> Set<OutdatedPackage>
private func getOutdatedCasks(from intermediaryArray: [OutdatedPackageCommandOutput.Casks], brewData: BrewDataStorage, forUpdatingType updatingType: OutdatedPackage.PackageUpdatingType) async -> Set<OutdatedPackage>
{
var finalOutdatedCaskTracker: Set<OutdatedPackage> = .init()
for outdatedCask in intermediaryArray
{
if let foundOutdatedCask = brewData.installedCasks.first(where: { $0.name == outdatedCask.name })
if let foundOutdatedCask = brewData.successfullyLoadedCasks.first(where: { $0.name == outdatedCask.name })
{
finalOutdatedCaskTracker.insert(.init(
package: foundOutdatedCask,
installedVersions: outdatedCask.installedVersions,
newerVersion: outdatedCask.currentVersion
newerVersion: outdatedCask.currentVersion,
updatingManagedBy: updatingType
)
)
}

View File

@ -0,0 +1,120 @@
//
// Check if Package Was Installed Intentionally.swift
// Cork
//
// Created by David Bureš on 13.11.2024.
//
import Foundation
import CorkShared
enum IntentionalInstallationDiscoveryError: Error, Hashable
{
/// The function could not determine the most relevant version of the package to read the recepit from
case failedToDetermineMostRelevantVersion(packageURL: URL)
/// The installation receipt is there, but cannot be read due to permission issues
case failedToReadInstallationRecepit(packageURL: URL)
/// The installation receipt could be read, but not parsed
case failedToParseInstallationReceipt(packageURL: URL)
/// The installation receipt is missing completely
case installationReceiptMissingCompletely(packageURL: URL)
/// The provided `URL` has an unexpected form
case unexpectedFolderName(packageURL: URL)
}
extension URL
{
/// This function checks whether the package was installed intentionally.
/// - For Formulae, this info gets read from the install receipt
/// - Casks are always instaled intentionally
/// - Parameter versionURLs: All available versions for this package. Some packages have multiple versions installed at a time (for example, the package `xz` might have versions 1.2 and 1.3 installed at once)
/// - Returns: Indication whether this package was installed intentionally or not
func checkIfPackageWasInstalledIntentionally(versionURLs: [URL]) async throws(IntentionalInstallationDiscoveryError) -> Bool
{
// TODO: Convert this so it uses the most recent version instead of a random one
guard let localPackagePath = versionURLs.first
else
{
throw .failedToDetermineMostRelevantVersion(packageURL: self)
// throw .failedWhileLoadingCertainPackage(lastPathComponent, self, failureReason: String(localized: "error.package-loading.could-not-load-version-to-check-from-available-versions"))
}
if path.contains("Cellar")
{
let localPackageInfoJSONPath: URL = localPackagePath.appendingPathComponent("INSTALL_RECEIPT.json", conformingTo: .json)
if FileManager.default.fileExists(atPath: localPackageInfoJSONPath.path)
{
struct InstallRecepitParser: Codable
{
let installedOnRequest: Bool
}
let decoder: JSONDecoder = {
let decoder: JSONDecoder = .init()
decoder.keyDecodingStrategy = .convertFromSnakeCase
return decoder
}()
do
{
let installReceiptContents: Data = try .init(contentsOf: localPackageInfoJSONPath)
do
{
return try decoder.decode(InstallRecepitParser.self, from: installReceiptContents).installedOnRequest
}
catch let installReceiptParsingError
{
AppConstants.shared.logger.error("Failed to decode install receipt for package \(self.lastPathComponent, privacy: .public) with error \(installReceiptParsingError.localizedDescription, privacy: .public)")
throw IntentionalInstallationDiscoveryError.failedToParseInstallationReceipt(packageURL: self)
// throw PackageLoadingError.failedWhileLoadingCertainPackage(self.lastPathComponent, self, failureReason: String(localized: "error.package-loading.could-not-decode-installa-receipt-\(installReceiptParsingError.localizedDescription)"))
}
}
catch let installReceiptLoadingError
{
AppConstants.shared.logger.error("Failed to load contents of install receipt for package \(self.lastPathComponent, privacy: .public) with error \(installReceiptLoadingError.localizedDescription, privacy: .public)")
throw .failedToReadInstallationRecepit(packageURL: self)
// throw .failedWhileLoadingCertainPackage(self.lastPathComponent, self, failureReason: String(localized: "error.package-loading.could-not-convert-contents-of-install-receipt-to-data-\(installReceiptLoadingError.localizedDescription)"))
}
}
else
{ /// There's no install receipt for this package - silently fail and return that the packagw was not installed intentionally
AppConstants.shared.logger.error("There appears to be no install receipt for package \(localPackageInfoJSONPath.lastPathComponent, privacy: .public)")
let shouldStrictlyCheckForHomebrewErrors: Bool = UserDefaults.standard.bool(forKey: "strictlyCheckForHomebrewErrors")
if shouldStrictlyCheckForHomebrewErrors
{
throw .installationReceiptMissingCompletely(packageURL: self)
// throw .failedWhileLoadingCertainPackage(lastPathComponent, self, failureReason: String(localized: "error.package-loading.missing-install-receipt"))
}
else
{
return false
}
}
}
else if path.contains("Caskroom")
{
return true
}
else
{
throw .unexpectedFolderName(packageURL: self)
// throw .failedWhileLoadingCertainPackage(lastPathComponent, self, failureReason: String(localized: "error.package-loading.unexpected-folder-name"))
}
}
}

View File

@ -0,0 +1,28 @@
//
// Filter Symlinks.swift
// Cork
//
// Created by David Bureš on 13.11.2024.
//
import Foundation
extension [URL]
{
/// Filter out all symlinks from an array of URLs
var withoutSymlinks: [URL]
{
return self.filter
{ url in
/// If the existence of a symlink cannot be verified, be safe and return `false`
guard let isSymlink = url.isSymlink()
else
{
return false
}
/// `isSymlink` is `true` for a symlink. Therefore, if we want to filter out symlinks, we have to return the opposite of `true`, which is `false`
return !isSymlink
}
}
}

View File

@ -0,0 +1,24 @@
//
// Get Package Type from URL.swift
// Cork
//
// Created by David Bureš on 13.11.2024.
//
import Foundation
extension URL
{
/// Determine a package's type type from its URL
var packageType: PackageType
{
if self.pathComponents.contains("Cellar")
{
return .formula
}
else
{
return .cask
}
}
}

View File

@ -0,0 +1,244 @@
//
// Load Up Installed Packages.swift
// Cork
//
// Created by David Bureš on 10.11.2024.
//
import CorkShared
import Foundation
/// A representation of the loaded ``BrewPackage``s
/// Includes packages that were loaded properly, along those whose loading failed
typealias BrewPackages = Set<Result<BrewPackage, PackageLoadingError>>
extension BrewDataStorage
{
/// Parent function for loading installed packages from disk
/// Abstracts away the function ``loadInstalledPackagesFromFolder(packageTypeToLoad:)``, transforming errors thrown by ``loadInstalledPackagesFromFolder(packageTypeToLoad:)`` into displayable errors
/// - Parameters:
/// - packageTypeToLoad: Which ``PackageType`` to load
/// - appState: ``AppState`` used to display loading errors
/// - Returns: A set of loaded ``BrewPackage``s for the specified ``PackageType``
func loadInstalledPackages(
packageTypeToLoad: PackageType, appState: AppState
) async -> BrewPackages?
{
/// Start tracking when loading started
let timeLoadingStarted: Date = .now
AppConstants.shared.logger.debug(
"Started \(packageTypeToLoad.rawValue, privacy: .public) loading task at \(timeLoadingStarted, privacy: .public)"
)
/// Calculate how long loading took
defer
{
AppConstants.shared.logger.debug("Finished \(packageTypeToLoad.rawValue, privacy: .public) loading task. Took \(timeLoadingStarted.timeIntervalSince(.now), privacy: .public)")
}
do
{
return try await self.loadInstalledPackagesFromFolder(
packageTypeToLoad: packageTypeToLoad)
}
catch let packageLoadingError
{
switch packageLoadingError
{
case .couldNotReadContentsOfParentFolder(let loadingError, let folderURL):
AppConstants.shared.logger.error("Failed while loading packages: Could not read contents of parent folder (\(folderURL.path()): \(loadingError)")
appState.showAlert(errorToShow: .couldNotGetContentsOfPackageFolder(loadingError))
case .packageDoesNotHaveAnyVersionsInstalled(let packageURL):
AppConstants.shared.logger.error("Failed while loading packages: Package \(packageURL.packageNameFromURL()) does not have any versions installed")
appState.showAlert(
errorToShow: .installedPackageHasNoVersions(
corruptedPackageName: packageURL.packageNameFromURL()))
case .packageIsNotAFolder(let offendingFile, let offendingFileURL):
AppConstants.shared.logger.error("Failed while loading packages: Package \(offendingFileURL.path()) is not a folder")
appState.showAlert(
errorToShow: .installedPackageIsNotAFolder(
itemName: offendingFile, itemURL: offendingFileURL
))
case .numberOLoadedPackagesDosNotMatchNumberOfPackageFolders:
AppConstants.shared.logger.error("Failed while loading packages: Number of loaded packages does not match the number of URLs in package folder")
appState.showAlert(errorToShow: .numberOfLoadedPackagesDoesNotMatchNumberOfPackageFolders)
case .triedToThreatFolderContainingPackagesAsPackage(let packageType):
appState.showAlert(errorToShow: .triedToThreatFolderContainingPackagesAsPackage(packageType: packageType))
case .failedWhileReadingContentsOfPackageFolder(let folderURL, let reportedError):
AppConstants.shared.logger.error("Failed while loading packages: Couldn't read contents of package folder \(folderURL) with this error: \(reportedError)")
case .failedWhileTryingToDetermineIntentionalInstallation(let folderURL, let associatedIntentionalDiscoveryError):
AppConstants.shared.logger.error("Failed while loading packages: Couldn't determine intentional installation status for package \(folderURL) with this error: \(associatedIntentionalDiscoveryError.localizedDescription)")
}
switch packageTypeToLoad
{
case .formula:
appState.failedWhileLoadingFormulae = true
case .cask:
appState.failedWhileLoadingCasks = true
}
return nil
}
}
}
private extension BrewDataStorage
{
/// Load packages from disk, and convert them into ``BrewPackage``s
func loadInstalledPackagesFromFolder(
packageTypeToLoad: PackageType
) async throws(PackageLoadingError) -> BrewPackages
{
do
{
/// 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])
AppConstants.shared.logger.debug("Loaded contents of folder: \(urlsInParentFolder)")
let packageLoader: BrewPackages = await withTaskGroup(of: Result<BrewPackage, PackageLoadingError>.self)
{ taskGroup in
for packageURL in urlsInParentFolder
{
AppConstants.shared.logger.debug("Will add package at URL \(packageURL) to the package loading task group")
taskGroup.addTask
{
await self.loadInstalledPackage(packageURL: packageURL)
}
/*
guard taskGroup.addTaskUnlessCancelled(priority: .high, operation: {
await self.loadInstalledPackage(packageURL: packageURL)
})
else
{
AppConstants.shared.logger.warning("Package loading task group got cancelled")
break
}
*/
}
var loadedPackages: BrewPackages = .init(minimumCapacity: urlsInParentFolder.count)
for await loadedPackage in taskGroup
{
AppConstants.shared.logger.debug("Will insert package \(loadedPackages) to the package result array")
loadedPackages.insert(loadedPackage)
}
let loggableLoadedPackages: String = loadedPackages.compactMap { rawResult in
switch rawResult {
case .success(let success):
return success.name
case .failure(let failure):
return failure.errorDescription
}
}.formatted(.list(type: .and))
AppConstants.shared.logger.debug("Loaded \(packageTypeToLoad.description): \(loggableLoadedPackages)")
return loadedPackages
}
let shouldStrictlyCheckForHomebrewErrors: Bool = UserDefaults.standard.bool(forKey: "strictlyCheckForHomebrewErrors")
if shouldStrictlyCheckForHomebrewErrors
{
/// Check if the number of loaded packages, both successful and failed, matches the number of package URLs in the package container folder
guard packageLoader.count == urlsInParentFolder.count
else
{
throw PackageLoadingError.numberOLoadedPackagesDosNotMatchNumberOfPackageFolders
}
}
return packageLoader
}
catch let parentFolderReadingError
{
AppConstants.shared.logger.error("Couldn't get contents of folder \(packageTypeToLoad.parentFolder, privacy: .public)")
throw .couldNotReadContentsOfParentFolder(failureReason: parentFolderReadingError.localizedDescription, folderURL: packageTypeToLoad.parentFolder)
}
}
/// For a given `URL` to a package folder containing the various versions of the package, parse the package contained within
/// - Parameter packageURL: `URL` to the package parent folder
/// - Returns: A parsed package of the ``BrewPackage`` type
func loadInstalledPackage(packageURL: URL) async -> Result<BrewPackage, PackageLoadingError>
{
/// Get the name of the package - at this stage, it is the last path component
let packageName: String = packageURL.packageNameFromURL()
AppConstants.shared.logger.debug("Package name to load: \(packageName). Will check if this is a legit package")
/// Check if we're not trying to read versions in the Cellar or Caskroom folder itself - this usually means Homebrew is broken
guard packageName != "Cellar", packageName != "Caskroom"
else
{
AppConstants.shared.logger.error("The last path component of the requested URL is the package container folder itself - perhaps a misconfigured package folder? Tried to load URL \(packageURL)")
return .failure(.triedToThreatFolderContainingPackagesAsPackage(packageType: packageURL.packageType))
}
AppConstants.shared.logger.debug("Package \(packageName) is legit. Will try to process it")
/// Let's try to parse the package now
do
{
/// 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])
guard !versionURLs.isEmpty else
{
AppConstants.shared.logger.error("Failed while loading package \(packageURL.packageNameFromURL()) because it has no versions installed")
return .failure(.packageDoesNotHaveAnyVersionsInstalled(packageURL: packageURL))
}
/// Gets the name of the version, which at this stage is the last path component of the `versionURLs` URL
let versionNamesForPackage: [String] = versionURLs.map
{ versionURL in
versionURL.lastPathComponent
}
AppConstants.shared.logger.debug("Package \(packageURL.lastPathComponent) has these versions available: \(versionURLs.map { $0.absoluteString }.joined(separator: ", "))")
do
{
AppConstants.shared.logger.debug("Will check if package \(packageName) was installed intentionally")
let wasPackageInstalledIntentionally: Bool = try await packageURL.checkIfPackageWasInstalledIntentionally(versionURLs: versionURLs)
AppConstants.shared.logger.debug("Package \(packageName) \(wasPackageInstalledIntentionally ? "was" : "was not") installed intentionally")
let loadedPackage: Result<BrewPackage, PackageLoadingError> = .success(
.init(
name: packageName,
type: packageURL.packageType,
installedOn: packageURL.creationDate,
versions: versionNamesForPackage,
installedIntentionally: wasPackageInstalledIntentionally,
sizeInBytes: packageURL.directorySize
)
)
return loadedPackage
}
catch let intentionalInstallationDiscoveryError
{
throw PackageLoadingError.failedWhileTryingToDetermineIntentionalInstallation(folderURL: packageURL, associatedIntentionalDiscoveryError: intentionalInstallationDiscoveryError)
}
}
catch let loadingError
{
AppConstants.shared.logger.error("Failed while loading package \(packageURL.lastPathComponent, privacy: .public): \(loadingError.localizedDescription)")
return .failure(.failedWhileReadingContentsOfPackageFolder(folderURL: packageURL, reportedError: loadingError.localizedDescription))
}
}
}

View File

@ -5,9 +5,10 @@
// Created by David Bureš on 11.02.2023.
//
import Foundation
import CorkShared
import Foundation
/*
@MainActor
func loadUpPackages(whatToLoad: PackageType, appState: AppState) async -> Set<BrewPackage>
{
@ -29,6 +30,8 @@ func loadUpPackages(whatToLoad: PackageType, appState: AppState) async -> Set<Br
{
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):
@ -48,3 +51,4 @@ func loadUpPackages(whatToLoad: PackageType, appState: AppState) async -> Set<Br
return contentsOfFolder
}
*/

View File

@ -98,7 +98,7 @@ extension BrewPackage
}
/// The stable files
let stable: Stable
let stable: Stable?
}
/// Info about the relevant files
@ -130,9 +130,14 @@ extension BrewPackage
}
}
func getCompatibility() -> Bool
func getCompatibility() -> Bool?
{
for compatibleSystem in bottle.stable.files.keys
guard let stable = bottle.stable else
{
AppConstants.shared.logger.debug("Package \(name) has unknown compatibility")
return nil
}
for compatibleSystem in stable.files.keys
{
if compatibleSystem.contains(AppConstants.shared.osVersionString.lookupName)
{

View File

@ -0,0 +1,54 @@
//
// Update Package Tracker In place.swift
// Cork
//
// Created by David Bureš on 03.01.2025.
//
import Foundation
extension BrewDataStorage
{
/// Update a ``BrewPackage``'s property in-place
/// Used to update the UI when a property on ``BrewPackage`` changes
@MainActor
func updatePackageInPlace(_ package: BrewPackage, modification: (inout BrewPackage) -> Void)
{
if package.type == .formula
{
var updatedFormulae: BrewPackages = installedFormulae
if let index = updatedFormulae.firstIndex(where: {
if case .success(let packageCopy) = $0
{
return packageCopy.id == package.id
}
return false
})
{
var updatedPackage: BrewPackage = package
modification(&updatedPackage)
updatedFormulae.remove(at: index)
updatedFormulae.insert(.success(updatedPackage))
installedFormulae = updatedFormulae
}
}
else
{
var updatedCasks: BrewPackages = installedCasks
if let index = updatedCasks.firstIndex(where: {
if case .success(let packageCopy) = $0
{
return packageCopy.id == package.id
}
return false
})
{
var updatedPackage: BrewPackage = package
modification(&updatedPackage)
updatedCasks.remove(at: index)
updatedCasks.insert(.success(updatedPackage))
installedCasks = updatedCasks
}
}
}
}

View File

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

View File

@ -14,11 +14,3 @@ struct SearchResults
let foundFormulae: [String]
let foundCasks: [String]
}
func getListOfFoundPackages(searchWord: String) async -> String
{
var parsedResponse: String?
parsedResponse = await shell(AppConstants.shared.brewExecutablePath, ["search", searchWord]).standardOutput
return parsedResponse!
}

View File

@ -52,6 +52,126 @@ func shell(
environment: [String: String]? = nil,
workingDirectory: URL? = nil
) -> AsyncStream<StreamedTerminalOutput>
{
let task: Process = .init()
var finalEnvironment: [String: String] = .init()
// MARK: - Set up the $HOME environment variable so brew commands work on versions 4.1 and up
if var environment
{
environment["HOME"] = FileManager.default.homeDirectoryForCurrentUser.path
finalEnvironment = environment
}
else
{
finalEnvironment = ["HOME": FileManager.default.homeDirectoryForCurrentUser.path]
}
// MARK: - Set up mirrors if the environment variables exist
if let brewApiDomain = ProcessInfo.processInfo.environment["HOMEBREW_API_DOMAIN"]
{
finalEnvironment["HOMEBREW_API_DOMAIN"] = brewApiDomain
}
if let brewBottleDomain = ProcessInfo.processInfo.environment["HOMEBREW_BOTTLE_DOMAIN"]
{
finalEnvironment["HOMEBREW_BOTTLE_DOMAIN"] = brewBottleDomain
}
// MARK: - Set up proxy if it's enabled
if let proxySettings = AppConstants.shared.proxySettings
{
AppConstants.shared.logger.info("Proxy is enabled")
finalEnvironment["ALL_PROXY"] = "\(proxySettings.host):\(proxySettings.port)"
}
// MARK: - Block automatic cleanup is configured
if !UserDefaults.standard.bool(forKey: "isAutomaticCleanupEnabled")
{
finalEnvironment["HOMEBREW_NO_INSTALL_CLEANUP"] = "TRUE"
}
AppConstants.shared.logger.debug("Final environment: \(finalEnvironment)")
// MARK: - Set working directory if provided
if let workingDirectory
{
AppConstants.shared.logger.info("Working directory configured: \(workingDirectory)")
task.currentDirectoryURL = workingDirectory
}
let sudoHelperURL: URL = Bundle.main.resourceURL!.appendingPathComponent("Sudo Helper", conformingTo: .executable)
finalEnvironment["SUDO_ASKPASS"] = sudoHelperURL.path
task.environment = finalEnvironment
task.launchPath = launchPath.absoluteString
/// Filter out empty things from the arguments so they don't fuck it up
task.arguments = arguments.filter({ $0 != "" })
let pipe: Pipe = .init()
task.standardOutput = pipe
let errorPipe: Pipe = .init()
task.standardError = errorPipe
do
{
try task.run()
}
catch
{
AppConstants.shared.logger.error("\(String(describing: error))")
}
return AsyncStream
{ continuation in
pipe.fileHandleForReading.readabilityHandler = { handler in
guard let standardOutput = String(data: handler.availableData, encoding: .utf8)
else
{
return
}
guard !standardOutput.isEmpty
else
{
return
}
continuation.yield(.standardOutput(standardOutput))
}
errorPipe.fileHandleForReading.readabilityHandler = { handler in
guard let errorOutput = String(data: handler.availableData, encoding: .utf8)
else
{
return
}
guard !errorOutput.isEmpty else { return }
continuation.yield(.standardError(errorOutput))
}
task.terminationHandler = { _ in
continuation.finish()
}
}
}
func shell(
_ launchPath: URL,
_ arguments: [String],
environment: [String: String]? = nil,
workingDirectory: URL? = nil
) -> (stream: AsyncStream<StreamedTerminalOutput>, process: Process)
{
let task: Process = .init()
@ -94,6 +214,11 @@ func shell(
{
finalEnvironment["HOMEBREW_NO_INSTALL_CLEANUP"] = "TRUE"
}
// MARK: - Automatically accept EULA if enabled
if UserDefaults.standard.bool(forKey: "automaticallyAcceptEULA") {
finalEnvironment["HOMEBREW_ACCEPT_EULA"] = "Y"
}
AppConstants.shared.logger.debug("Final environment: \(finalEnvironment)")
@ -111,7 +236,9 @@ func shell(
task.environment = finalEnvironment
task.launchPath = launchPath.absoluteString
task.arguments = arguments
/// Filter out empty things from the arguments so they don't fuck it up
task.arguments = arguments.filter({ $0 != "" })
let pipe: Pipe = .init()
task.standardOutput = pipe
@ -128,7 +255,7 @@ func shell(
AppConstants.shared.logger.error("\(String(describing: error))")
}
return AsyncStream
let stream: AsyncStream<StreamedTerminalOutput> = AsyncStream
{ continuation in
pipe.fileHandleForReading.readabilityHandler = { handler in
guard let standardOutput = String(data: handler.availableData, encoding: .utf8)
@ -162,4 +289,5 @@ func shell(
continuation.finish()
}
}
return (stream, task)
}

View File

@ -8,30 +8,43 @@
import Foundation
import CorkShared
@MainActor
func applyTagsToPackageTrackingArray(appState: AppState, brewData: BrewDataStorage) async throws
extension BrewDataStorage
{
for taggedName in appState.taggedPackageNames
@MainActor
func applyTags(appState: AppState) async throws
{
AppConstants.shared.logger.log("Will attempt to place package name \(taggedName, privacy: .public)")
brewData.installedFormulae = Set(brewData.installedFormulae.map
{ formula in
var copyFormula: BrewPackage = formula
if copyFormula.name == taggedName
{
copyFormula.changeTaggedStatus()
}
return copyFormula
})
for taggedName in appState.taggedPackageNames
{
AppConstants.shared.logger.log("Will attempt to place package name \(taggedName, privacy: .public)")
self.installedFormulae = Set(self.installedFormulae.map
{ formula in
switch formula
{
case .success(var brewPackage):
if brewPackage.name == taggedName
{
brewPackage.changeTaggedStatus()
}
return .success(brewPackage)
case .failure(let error):
return .failure(error)
}
})
brewData.installedCasks = Set(brewData.installedCasks.map
{ cask in
var copyCask: BrewPackage = cask
if copyCask.name == taggedName
{
copyCask.changeTaggedStatus()
}
return copyCask
})
self.installedCasks = Set(self.installedCasks.map
{ cask in
switch cask
{
case .success(var brewPackage):
if brewPackage.name == taggedName
{
brewPackage.changeTaggedStatus()
}
return .success(brewPackage)
case .failure(let error):
return .failure(error)
}
})
}
}
}

View File

@ -1,51 +0,0 @@
//
// Tag Package.swift
// Cork
//
// Created by David Bureš on 21.03.2023.
//
import Foundation
import CorkShared
@MainActor
func changePackageTagStatus(package: BrewPackage, brewData: BrewDataStorage, appState: AppState) async
{
if package.type == .formula
{
brewData.installedFormulae = Set(brewData.installedFormulae.map
{ formula in
var copyFormula: BrewPackage = formula
if copyFormula.name == package.name
{
copyFormula.changeTaggedStatus()
}
return copyFormula
})
}
else
{
brewData.installedFormulae = Set(brewData.installedCasks.map
{ cask in
var copyCask: BrewPackage = cask
if copyCask.name == package.name
{
copyCask.changeTaggedStatus()
}
return copyCask
})
}
if appState.taggedPackageNames.contains(package.name)
{
appState.taggedPackageNames.remove(package.name)
}
else
{
appState.taggedPackageNames.insert(package.name)
}
AppConstants.shared.logger.debug("Tagged package with ID \(package.id, privacy: .public): \(package.name, privacy: .public)")
AppConstants.shared.logger.debug("Tagged packages: \(appState.taggedPackageNames, privacy: .public)")
}

View File

@ -24,7 +24,7 @@ enum UntapError: LocalizedError
}
@MainActor
func removeTap(name: String, availableTaps: AvailableTaps, appState: AppState, shouldApplyUninstallSpinnerToRelevantItemInSidebar: Bool = false) async throws
func removeTap(name: String, availableTaps: TapTracker, appState: AppState, shouldApplyUninstallSpinnerToRelevantItemInSidebar: Bool = false) async throws
{
var indexToReplaceGlobal: Int?

View File

@ -13,8 +13,9 @@ import CorkShared
func updatePackages(updateProgressTracker: UpdateProgressTracker, detailStage: UpdatingProcessDetails) async
{
let showRealTimeTerminalOutputs: Bool = UserDefaults.standard.bool(forKey: "showRealTimeTerminalOutputOfOperations")
let includeGreedyPackages: Bool = UserDefaults.standard.bool(forKey: "includeGreedyOutdatedPackages")
for await output in shell(AppConstants.shared.brewExecutablePath, ["upgrade"])
for await output in shell(AppConstants.shared.brewExecutablePath, ["upgrade", includeGreedyPackages ? "--greedy" : ""])
{
switch output
{

View File

@ -11,21 +11,85 @@ import SwiftUI
@MainActor
class BrewDataStorage: ObservableObject
{
@Published var installedFormulae: Set<BrewPackage> = .init()
@Published var installedCasks: Set<BrewPackage> = .init()
@Published var installedFormulae: BrewPackages = .init()
@Published var installedCasks: BrewPackages = .init()
// MARK: - Successfully loaded packages
/// Formulae that were successfuly loaded from disk
var successfullyLoadedFormulae: Set<BrewPackage>
{
return Set(installedFormulae.compactMap
{ rawResult in
if case .success(let success) = rawResult
{
return success
}
else
{
return nil
}
})
}
/// Collected errors from failed Formulae loading
var unsuccessfullyLoadedFormulaeErrors: [PackageLoadingError]
{
return installedFormulae.compactMap
{ rawResult in
if case .failure(let failure) = rawResult {
return failure
}
else
{
return nil
}
}
}
/// Casks that were successfuly loaded from disk
var successfullyLoadedCasks: Set<BrewPackage>
{
return Set(installedCasks.compactMap
{ rawResult in
if case .success(let success) = rawResult
{
return success
}
else
{
return nil
}
})
}
/// Collected errors from failed Casks loading
var unsuccessfullyLoadedCasksErrors: [PackageLoadingError]
{
return installedCasks.compactMap
{ rawResult in
if case .failure(let failure) = rawResult {
return failure
}
else
{
return nil
}
}
}
func insertPackageIntoTracker(_ package: BrewPackage)
{
if package.type == .formula
{
installedFormulae.insert(package)
installedFormulae.insert(.success(package))
}
else
{
installedCasks.insert(package)
installedCasks.insert(.success(package))
}
}
/*
func removeFormulaFromTracker(withName name: String)
{
removePackageFromTracker(withName: name, tracker: .formula)
@ -52,10 +116,38 @@ class BrewDataStorage: ObservableObject
}
}
}
*/
}
extension BrewDataStorage
{
var numberOfInstalledFormulae: Int
{
let displayOnlyIntentionallyInstalledPackagesByDefault: Bool = UserDefaults.standard.bool(forKey: "displayOnlyIntentionallyInstalledPackagesByDefault")
if displayOnlyIntentionallyInstalledPackagesByDefault
{
return self.successfullyLoadedFormulae.filter(\.installedIntentionally).count
}
else
{
return self.successfullyLoadedFormulae.count
}
}
var numberOfInstalledCasks: Int
{
return self.successfullyLoadedCasks.count
}
var numberOfInstalledPackages: Int
{
return self.numberOfInstalledFormulae + self.numberOfInstalledCasks
}
}
@MainActor
class AvailableTaps: ObservableObject
class TapTracker: ObservableObject
{
@Published var addedTaps: [BrewTap] = .init()
}

View File

@ -0,0 +1,43 @@
//
// Assign Types to Cached Packages.swift
// Cork
//
// Created by David Bureš - P on 16.01.2025.
//
import Foundation
import CorkShared
extension CachedPackagesTracker
{
@MainActor
func assignPackageTypeToCachedDownloads(brewData: BrewDataStorage)
{
var cachedDownloadsTracker: [CachedDownload] = .init()
AppConstants.shared.logger.debug("Package tracker in cached download assignment function has \(brewData.installedFormulae.count + brewData.installedCasks.count) packages")
for cachedDownload in cachedDownloads
{
let normalizedCachedPackageName: String = cachedDownload.packageName.onlyLetters
if brewData.successfullyLoadedFormulae.contains(where: { $0.name.localizedCaseInsensitiveContains(normalizedCachedPackageName) })
{ /// The cached package is a formula
AppConstants.shared.logger.debug("Cached package \(cachedDownload.packageName) (\(normalizedCachedPackageName)) is a formula")
cachedDownloadsTracker.append(.init(packageName: cachedDownload.packageName, sizeInBytes: cachedDownload.sizeInBytes, packageType: .formula))
}
else if brewData.successfullyLoadedCasks.contains(where: { $0.name.localizedCaseInsensitiveContains(normalizedCachedPackageName) })
{ /// The cached package is a cask
AppConstants.shared.logger.debug("Cached package \(cachedDownload.packageName) (\(normalizedCachedPackageName)) is a cask")
cachedDownloadsTracker.append(.init(packageName: cachedDownload.packageName, sizeInBytes: cachedDownload.sizeInBytes, packageType: .cask))
}
else
{ /// The cached package cannot be found
AppConstants.shared.logger.debug("Cached package \(cachedDownload.packageName) (\(normalizedCachedPackageName)) is unknown")
cachedDownloadsTracker.append(.init(packageName: cachedDownload.packageName, sizeInBytes: cachedDownload.sizeInBytes, packageType: .unknown))
}
}
cachedDownloads = cachedDownloadsTracker
}
}

View File

@ -0,0 +1,23 @@
//
// Cached Packages Tracker.swift
// Cork
//
// Created by David Bureš - P on 16.01.2025.
//
import Foundation
import SwiftUI
import CorkShared
class CachedPackagesTracker: ObservableObject
{
@Published 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
{
return cachedDownloads.reduce(0) { $0 + $1.sizeInBytes }
}
}

View File

@ -7,7 +7,7 @@
import Foundation
struct CorruptedPackage: Identifiable
struct CorruptedPackage: Identifiable, Equatable
{
let id: UUID = .init()
let name: String

View File

@ -12,18 +12,34 @@ import SwiftUI
class OutdatedPackageTracker: ObservableObject, Sendable
{
@AppStorage("displayOnlyIntentionallyInstalledPackagesByDefault") var displayOnlyIntentionallyInstalledPackagesByDefault: Bool = true
@AppStorage("includeGreedyOutdatedPackages") var includeGreedyOutdatedPackages: Bool = false
@Published var outdatedPackages: Set<OutdatedPackage> = .init()
var displayableOutdatedPackages: Set<OutdatedPackage>
{
if displayOnlyIntentionallyInstalledPackagesByDefault
/// Depending on whether greedy updating is enabled:
/// - If enabled, include packages that are also self-updating
/// - If disabled, include only packages whose updates are managed by Homebrew
var relevantOutdatedPackages: Set<OutdatedPackage>
if includeGreedyOutdatedPackages
{
return outdatedPackages.filter(\.package.installedIntentionally)
relevantOutdatedPackages = outdatedPackages
}
else
{
return outdatedPackages
relevantOutdatedPackages = outdatedPackages.filter{ $0.updatingManagedBy == .homebrew }
}
if displayOnlyIntentionallyInstalledPackagesByDefault
{
return relevantOutdatedPackages.filter(\.package.installedIntentionally)
}
else
{
return relevantOutdatedPackages
}
}
}

View File

@ -9,6 +9,26 @@ 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
@ -17,4 +37,15 @@ struct OutdatedPackage: Identifiable, Equatable, Hashable
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

@ -15,14 +15,30 @@ class InstallationProgressTracker: ObservableObject
@Published var numberOfPackageDependencies: Int = 0
@Published var numberInLineOfPackageCurrentlyBeingFetched: Int = 0
@Published var numberInLineOfPackageCurrentlyBeingInstalled: Int = 0
private var installationProcess: Process?
private var showRealTimeTerminalOutputs: Bool
{
UserDefaults.standard.bool(forKey: "showRealTimeTerminalOutputOfOperations")
}
deinit
{
cancel()
}
@discardableResult
func cancel() -> Bool
{
guard let installationProcess else {return false}
installationProcess.terminate()
self.installationProcess = nil
return true
}
@MainActor
func installPackage(using brewData: BrewDataStorage) async throws -> TerminalOutput
func installPackage(using brewData: BrewDataStorage, cachedPackagesTracker: CachedPackagesTracker) async throws -> TerminalOutput
{
let package: BrewPackage = packageBeingInstalled.package
@ -48,7 +64,14 @@ class InstallationProgressTracker: ObservableObject
try await installCask(using: brewData)
}
await synchronizeInstalledPackages(brewData: brewData)
do
{
try await brewData.synchronizeInstalledPackages(cachedPackagesTracker: cachedPackagesTracker)
}
catch let synchronizationError
{
AppConstants.shared.logger.error("Package isntallation function failed to synchronize packages: \(synchronizationError.localizedDescription)")
}
return installationResult
}
@ -64,7 +87,9 @@ class InstallationProgressTracker: ObservableObject
AppConstants.shared.logger.info("Package \(package.name, privacy: .public) is Formula")
for await output in shell(AppConstants.shared.brewExecutablePath, ["install", package.name])
let (stream, process): (AsyncStream<StreamedTerminalOutput>, Process) = shell(AppConstants.shared.brewExecutablePath, ["install", package.name])
installationProcess = process
for await output in stream
{
switch output
{
@ -179,7 +204,9 @@ class InstallationProgressTracker: ObservableObject
AppConstants.shared.logger.info("Package is Cask")
AppConstants.shared.logger.debug("Installing package \(package.name, privacy: .public)")
for await output in shell(AppConstants.shared.brewExecutablePath, ["install", "--no-quarantine", package.name])
let (stream, process): (AsyncStream<StreamedTerminalOutput>, Process) = shell(AppConstants.shared.brewExecutablePath, ["install", "--no-quarantine", package.name])
installationProcess = process
for await output in stream
{
switch output
{

View File

@ -39,7 +39,7 @@ class BrewPackageDetails: ObservableObject
let outdated: Bool
let caveats: String?
let isCompatible: Bool
let isCompatible: Bool?
// MARK: - Mutable properties
@ -48,7 +48,7 @@ class BrewPackageDetails: ObservableObject
// MARK: - Init
init(name: String, description: String?, homepage: URL, tap: BrewTap, installedAsDependency: Bool, dependents: [String]? = nil, dependencies: [BrewPackageDependency]? = nil, outdated: Bool, caveats: String? = nil, pinned: Bool, isCompatible: Bool)
init(name: String, description: String?, homepage: URL, tap: BrewTap, installedAsDependency: Bool, dependents: [String]? = nil, dependencies: [BrewPackageDependency]? = nil, outdated: Bool, caveats: String? = nil, pinned: Bool, isCompatible: Bool?)
{
self.name = name
self.description = description

View File

@ -0,0 +1,29 @@
//
// AsyncButton - Plain Style.swift
// Cork
//
// Created by David Bureš - P on 25.01.2025.
//
import SwiftUI
import ButtonKit
/// Style that doesn' change the text of the button, and disables it when the async operation is in progress
struct PlainAsyncButtonStyle: AsyncButtonStyle
{
init() {}
func makeLabel(configuration: LabelConfiguration) -> some View {
configuration.label
.disabledWhenLoading()
.asyncButtonStyle(.none)
}
}
extension AsyncButtonStyle where Self == PlainAsyncButtonStyle
{
static var plainStyle: PlainAsyncButtonStyle
{
PlainAsyncButtonStyle()
}
}

View File

@ -0,0 +1,31 @@
//
// Error Inspector.swift
// Cork
//
// Created by David Bureš - P on 20.01.2025.
//
import SwiftUI
struct ErrorInspector: View
{
let errorText: String
var body: some View
{
NavigationStack
{
TextEditor(text: .constant(errorText))
.navigationTitle("error-inspector.title")
.frame(minWidth: 300, minHeight: 200)
}
.modify
{ viewProxy in
if #available(macOS 15, *)
{
viewProxy
.windowMinimizeBehavior(.disabled)
}
}
}
}

View File

@ -5,9 +5,10 @@
// Created by David Bureš on 03.07.2022.
//
import SwiftUI
import CorkShared
import CorkNotifications
import CorkShared
import SwiftUI
import ButtonKit
struct AddFormulaView: View
{
@ -18,6 +19,8 @@ struct AddFormulaView: View
@EnvironmentObject var brewData: BrewDataStorage
@EnvironmentObject var appState: AppState
@EnvironmentObject var cachedDownloadsTracker: CachedPackagesTracker
@State private var foundPackageSelection: UUID? = nil
@ObservedObject var searchResultTracker: SearchResultTracker = .init()
@ -32,168 +35,141 @@ struct AddFormulaView: View
@AppStorage("showPackagesStillLeftToInstall") var showPackagesStillLeftToInstall: Bool = false
@AppStorage("notifyAboutPackageInstallationResults") var notifyAboutPackageInstallationResults: Bool = false
var shouldShowSheetTitle: Bool
{
[.ready, .presentingSearchResults].contains(packageInstallationProcessStep)
}
var isDismissable: Bool
{
[.ready, .presentingSearchResults, .fatalError, .anotherProcessAlreadyRunning, .binaryAlreadyExists, .requiresSudoPassword, .wrongArchitecture, .anotherProcessAlreadyRunning, .installationTerminatedUnexpectedly, .installing].contains(packageInstallationProcessStep)
}
var sheetTitle: LocalizedStringKey
{
switch packageInstallationProcessStep
{
case .ready:
return "add-package.title"
case .searching:
return ""
case .presentingSearchResults:
return "add-package.title"
case .installing:
return ""
case .finished:
return ""
case .fatalError:
return ""
case .requiresSudoPassword:
return ""
case .wrongArchitecture:
return ""
case .binaryAlreadyExists:
return ""
case .anotherProcessAlreadyRunning:
return ""
case .installationTerminatedUnexpectedly:
return ""
}
}
var body: some View
{
VStack(alignment: .leading, spacing: 10)
NavigationStack
{
switch packageInstallationProcessStep
SheetTemplate(isShowingTitle: shouldShowSheetTitle)
{
case .ready:
SheetWithTitle(title: "add-package.title")
Group
{
InstallationInitialView(
searchResultTracker: searchResultTracker,
packageRequested: $packageRequested,
foundPackageSelection: $foundPackageSelection,
installationProgressTracker: installationProgressTracker,
packageInstallationProcessStep: $packageInstallationProcessStep
)
}
case .searching:
InstallationSearchingView(
packageRequested: $packageRequested,
searchResultTracker: searchResultTracker,
packageInstallationProcessStep: $packageInstallationProcessStep
)
case .presentingSearchResults:
PresentingSearchResultsView(
searchResultTracker: searchResultTracker,
packageRequested: $packageRequested,
foundPackageSelection: $foundPackageSelection,
packageInstallationProcessStep: $packageInstallationProcessStep,
installationProgressTracker: installationProgressTracker
)
case .installing:
InstallingPackageView(
installationProgressTracker: installationProgressTracker,
packageInstallationProcessStep: $packageInstallationProcessStep
)
case .finished:
DisappearableSheet
{
ComplexWithIcon(systemName: "checkmark.seal")
switch packageInstallationProcessStep
{
HeadlineWithSubheadline(
headline: "add-package.finished",
subheadline: "add-package.finished.description",
alignment: .leading
case .ready:
InstallationInitialView(
searchResultTracker: searchResultTracker,
packageRequested: $packageRequested,
foundPackageSelection: $foundPackageSelection,
installationProgressTracker: installationProgressTracker,
packageInstallationProcessStep: $packageInstallationProcessStep
)
}
}
.onAppear
{
appState.cachedDownloadsFolderSize = AppConstants.shared.brewCachedDownloadsPath.directorySize
if notifyAboutPackageInstallationResults
{
sendNotification(title: String(localized: "notification.install-finished"))
}
}
case .fatalError: /// This shows up when the function for executing the install action throws an error
VStack(alignment: .leading)
{
ComplexWithIcon(systemName: "exclamationmark.triangle")
{
HeadlineWithSubheadline(
headline: "add-package.fatal-error-\(installationProgressTracker.packageBeingInstalled.package.name)",
subheadline: "add-package.fatal-error.description",
alignment: .leading
case .searching:
InstallationSearchingView(
packageRequested: $packageRequested,
searchResultTracker: searchResultTracker,
packageInstallationProcessStep: $packageInstallationProcessStep
)
}
HStack
{
Button
{
restartApp()
} label: {
Text("action.restart")
}
case .presentingSearchResults:
PresentingSearchResultsView(
searchResultTracker: searchResultTracker,
packageRequested: $packageRequested,
foundPackageSelection: $foundPackageSelection,
packageInstallationProcessStep: $packageInstallationProcessStep,
installationProgressTracker: installationProgressTracker
)
Spacer()
case .installing:
InstallingPackageView(
installationProgressTracker: installationProgressTracker,
packageInstallationProcessStep: $packageInstallationProcessStep
)
DismissSheetButton()
case .finished:
InstallationFinishedSuccessfullyView()
case .fatalError: /// This shows up when the function for executing the install action throws an error
InstallationFatalErrorView(installationProgressTracker: installationProgressTracker)
case .requiresSudoPassword:
SudoRequiredView(installationProgressTracker: installationProgressTracker)
case .wrongArchitecture:
WrongArchitectureView(installationProgressTracker: installationProgressTracker)
case .binaryAlreadyExists:
BinaryAlreadyExistsView(installationProgressTracker: installationProgressTracker)
case .anotherProcessAlreadyRunning:
AnotherProcessAlreadyRunningView()
case .installationTerminatedUnexpectedly:
InstallationTerminatedUnexpectedlyView(terminalOutputOfTheInstallation: installationProgressTracker.packageBeingInstalled.realTimeTerminalOutput)
}
}
case .requiresSudoPassword:
SudoRequiredView(installationProgressTracker: installationProgressTracker)
case .wrongArchitecture:
WrongArchitectureView(installationProgressTracker: installationProgressTracker)
case .binaryAlreadyExists:
BinaryAlreadyExistsView(installationProgressTracker: installationProgressTracker)
case .anotherProcessAlreadyRunning:
VStack(alignment: .leading)
.navigationTitle(sheetTitle)
.toolbar
{
ComplexWithImage(image: Image(localURL: URL(string: "/System/Library/CoreServices/KeyboardSetupAssistant.app/Contents/Resources/AppIcon.icns")!)!)
if isDismissable
{
VStack(alignment: .leading, spacing: 10)
ToolbarItem(placement: .cancellationAction)
{
Text("add-package.install.another-homebrew-process-blocking-install.title")
.font(.headline)
Text("add-package.install.another-homebrew-process-blocking-install.description")
HStack
AsyncButton
{
Button("add-package.clear-brew-locks", role: .destructive)
dismiss()
installationProgressTracker.cancel()
do
{
if let contentsOfLockFolder = try? FileManager.default.contentsOfDirectory(at: URL(fileURLWithPath: "/usr/local/var/homebrew/locks"), includingPropertiesForKeys: [.isRegularFileKey])
{
for lockURL in contentsOfLockFolder
{
try? FileManager.default.removeItem(at: lockURL)
}
}
dismiss()
try await brewData.synchronizeInstalledPackages(cachedPackagesTracker: cachedDownloadsTracker)
}
Spacer()
DismissSheetButton()
catch let synchronizationError
{
appState.showAlert(errorToShow: .couldNotSynchronizePackages(error: synchronizationError.localizedDescription))
}
} label: {
Text("action.cancel")
}
.keyboardShortcut(.cancelAction)
.disabledWhenLoading()
}
}
}
.fixedSize()
/*
default:
VStack(alignment: .leading)
{
ComplexWithIcon(systemName: "wifi.exclamationmark")
{
HeadlineWithSubheadline(
headline: "add-package.network-error",
subheadline: "add-package.network-error.description",
alignment: .leading
)
}
HStack
{
Spacer()
DismissSheetButton()
}
}
*/
case .installationTerminatedUnexpectedly:
InstallationTerminatedUnexpectedlyView(terminalOutputOfTheInstallation: installationProgressTracker.packageBeingInstalled.realTimeTerminalOutput)
}
}
.padding()
.fixedSize() // TODO: Remove this fixedSize later
.onDisappear
{
appState.assignPackageTypeToCachedDownloads(brewData: brewData)
cachedDownloadsTracker.assignPackageTypeToCachedDownloads(brewData: brewData)
}
}
}

View File

@ -16,7 +16,9 @@ struct SearchResultRow: View, Sendable
@EnvironmentObject var brewData: BrewDataStorage
let searchedForPackage: BrewPackage
let context: Self.Context
let downloadCount: Int?
@State private var description: String?
@State private var isCompatible: Bool?
@ -30,35 +32,46 @@ struct SearchResultRow: View, Sendable
HStack(alignment: .center)
{
SanitizedPackageName(packageName: searchedForPackage.name, shouldShowVersion: true)
if searchedForPackage.type == .formula
switch context
{
if brewData.installedFormulae.contains(where: { $0.name == searchedForPackage.name })
case .topPackages:
Spacer()
if let downloadCount
{
PillTextWithLocalizableText(localizedText: "add-package.result.already-installed")
Text("add-package.top-packages.list-item-\(downloadCount)")
.foregroundStyle(.secondary)
.font(.caption)
}
}
else
{
if brewData.installedCasks.contains(where: { $0.name == searchedForPackage.name })
case .searchResults:
if searchedForPackage.type == .formula
{
PillTextWithLocalizableText(localizedText: "add-package.result.already-installed")
}
}
if let isCompatible
{
if !isCompatible
{
if showCompatibilityWarning
if brewData.successfullyLoadedFormulae.contains(where: { $0.name == searchedForPackage.name })
{
HStack(alignment: .center, spacing: 4)
PillTextWithLocalizableText(localizedText: "add-package.result.already-installed")
}
}
else
{
if brewData.successfullyLoadedCasks.contains(where: { $0.name == searchedForPackage.name })
{
PillTextWithLocalizableText(localizedText: "add-package.result.already-installed")
}
}
if let isCompatible
{
if !isCompatible
{
if showCompatibilityWarning
{
Image(systemName: "exclamationmark.circle")
Text("add-package.result.not-optimized-for-\(AppConstants.shared.osVersionString.fullName)")
.font(.subheadline)
.foregroundColor(.red)
}
.font(.subheadline)
.foregroundColor(.red)
}
}
}
@ -137,4 +150,9 @@ struct SearchResultRow: View, Sendable
}
}
}
enum Context {
case searchResults
case topPackages
}
}

View File

@ -47,9 +47,9 @@ struct InstallationInitialView: View
{
List(selection: $foundPackageSelection)
{
TopPackagesSection(packageTracker: topPackagesTracker.sortedTopFormulae, trackerType: .formula)
TopPackagesSection(packageTracker: topPackagesTracker, trackerType: .formula)
TopPackagesSection(packageTracker: topPackagesTracker.sortedTopCasks, trackerType: .cask)
TopPackagesSection(packageTracker: topPackagesTracker, trackerType: .cask)
}
.listStyle(.bordered(alternatesRowBackgrounds: true))
.frame(minHeight: 200)
@ -72,58 +72,22 @@ struct InstallationInitialView: View
{
foundPackageSelection = nil // Clear all selected items when the user looks for a different package
}
HStack
}
.toolbar
{
if enableDiscoverability
{
DismissSheetButton()
Spacer()
if enableDiscoverability
ToolbarItemGroup(placement: .automatic)
{
PreviewPackageButtonWithCustomAction
{
guard let packageToPreview: BrewPackage = getTopPackageFromTracker() else
{
AppConstants.shared.logger.error("Could not retrieve top package to preview")
return
}
openWindow(value: packageToPreview)
}
.disabled(foundPackageSelection == nil)
previewPackageButton
Button
{
guard let packageToInstall: BrewPackage = getTopPackageFromTracker() else
{
AppConstants.shared.logger.error("Could not retrieve top package to install")
return
}
installationProgressTracker.packageBeingInstalled = PackageInProgressOfBeingInstalled(package: packageToInstall, installationStage: .ready, packageInstallationProgress: 0)
AppConstants.shared.logger.debug("Packages to install: \(installationProgressTracker.packageBeingInstalled.package.name, privacy: .public)")
packageInstallationProcessStep = .installing
} label: {
Text("add-package.install.action")
}
.keyboardShortcut(foundPackageSelection != nil ? .defaultAction : .init(.end))
.disabled(foundPackageSelection == nil)
startInstallProcessForTopPackageButton
}
Button
{
packageInstallationProcessStep = .searching
} label: {
Text("add-package.search.action")
}
.keyboardShortcut(foundPackageSelection == nil ? .defaultAction : .init(.end))
.disabled(packageRequested.isEmpty)
}
ToolbarItem(placement: .primaryAction)
{
searchForPackageButton
}
}
.onAppear
@ -132,6 +96,61 @@ struct InstallationInitialView: View
}
}
@ViewBuilder
var previewPackageButton: some View
{
PreviewPackageButtonWithCustomAction
{
guard let packageToPreview: BrewPackage = getTopPackageFromTracker() else
{
AppConstants.shared.logger.error("Could not retrieve top package to preview")
return
}
openWindow(value: packageToPreview)
}
.disabled(foundPackageSelection == nil)
}
@ViewBuilder
var startInstallProcessForTopPackageButton: some View
{
Button
{
guard let packageToInstall: BrewPackage = getTopPackageFromTracker() else
{
AppConstants.shared.logger.error("Could not retrieve top package to install")
return
}
installationProgressTracker.packageBeingInstalled = PackageInProgressOfBeingInstalled(package: packageToInstall, installationStage: .ready, packageInstallationProgress: 0)
AppConstants.shared.logger.debug("Packages to install: \(installationProgressTracker.packageBeingInstalled.package.name, privacy: .public)")
packageInstallationProcessStep = .installing
} label: {
Text("add-package.install.action")
}
.keyboardShortcut(foundPackageSelection != nil ? .defaultAction : .init(.end))
.disabled(foundPackageSelection == nil)
}
@ViewBuilder
var searchForPackageButton: some View
{
Button
{
packageInstallationProcessStep = .searching
} label: {
Text("add-package.search.action")
}
.keyboardShortcut(foundPackageSelection == nil ? .defaultAction : .init(.end))
.disabled(packageRequested.isEmpty)
}
func getTopPackageFromTracker() -> BrewPackage?
{
if let foundPackageSelection

View File

@ -1,27 +0,0 @@
//
// Top Package List Item.swift
// Cork
//
// Created by David Bureš on 30.08.2023.
//
import SwiftUI
struct TopPackageListItem: View
{
let topPackage: TopPackage
var body: some View
{
HStack(alignment: .center)
{
SanitizedPackageName(packageName: topPackage.packageName, shouldShowVersion: true)
Spacer()
Text("add-package.top-packages.list-item-\(topPackage.packageDownloads)")
.foregroundStyle(.secondary)
.font(.caption)
}
}
}

View File

@ -11,9 +11,26 @@ struct TopPackagesSection: View
{
@EnvironmentObject var brewData: BrewDataStorage
let packageTracker: [TopPackage]
let packageTracker: TopPackagesTracker
let trackerType: PackageType
private var packages: [TopPackage]
{
switch trackerType
{
case .formula:
packageTracker.sortedTopFormulae.filter
{
!brewData.successfullyLoadedFormulae.map(\.name).contains($0.packageName)
}
case .cask:
packageTracker.sortedTopCasks.filter
{
!brewData.successfullyLoadedCasks.map(\.name).contains($0.packageName)
}
}
}
@State private var isCollapsed: Bool = false
@ -23,19 +40,9 @@ struct TopPackagesSection: View
{
if !isCollapsed
{
ForEach(packageTracker.filter
{
switch trackerType
{
case .formula:
!brewData.installedFormulae.map(\.name).contains($0.packageName)
case .cask:
!brewData.installedCasks.map(\.name).contains($0.packageName)
}
}.prefix(15))
{ topFormula in
TopPackageListItem(topPackage: topFormula)
ForEach(packages.prefix(15))
{ topPackage in
SearchResultRow(searchedForPackage: BrewPackage(name: topPackage.packageName, type: trackerType, installedOn: nil, versions: [], sizeInBytes: nil), context: .topPackages, downloadCount: topPackage.packageDownloads)
}
}
} header: {

View File

@ -14,6 +14,8 @@ struct InstallingPackageView: View
@EnvironmentObject var appState: AppState
@EnvironmentObject var brewData: BrewDataStorage
@EnvironmentObject var cachedPackagesTracker: CachedPackagesTracker
@ObservedObject var installationProgressTracker: InstallationProgressTracker
@ -111,11 +113,15 @@ struct InstallingPackageView: View
}
}
}
.task(priority: .userInitiated)
.task
{
do
{
let installationResult: TerminalOutput = try await installationProgressTracker.installPackage(using: brewData)
let installationResult: TerminalOutput = try await installationProgressTracker.installPackage(
using: brewData,
cachedPackagesTracker: cachedPackagesTracker
)
AppConstants.shared.logger.debug("Installation result:\nStandard output: \(installationResult.standardOutput, privacy: .public)\nStandard error: \(installationResult.standardError, privacy: .public)")
/// Check if the package installation stag at the end of the install process was something unexpected. Normal package installations go through multiple steps, and the three listed below are not supposed to be the end state. This means that something went wrong during the installation

View File

@ -33,11 +33,6 @@ struct PresentingSearchResultsView: View
{
VStack
{
InstallProcessCustomSearchField(search: $packageRequested, isFocused: $isSearchFieldFocused, customPromptText: String(localized: "add-package.search.prompt"))
{
foundPackageSelection = nil // Clear all selected items when the user looks for a different package
}
List(selection: $foundPackageSelection)
{
SearchResultsSection(
@ -51,56 +46,79 @@ struct PresentingSearchResultsView: View
)
}
.listStyle(.bordered(alternatesRowBackgrounds: true))
.frame(width: 300, height: 300)
.frame(minHeight: 200)
HStack
InstallProcessCustomSearchField(search: $packageRequested, isFocused: $isSearchFieldFocused, customPromptText: String(localized: "add-package.search.prompt"))
{
DismissSheetButton()
Spacer()
PreviewPackageButtonWithCustomAction
{
do
{
let requestedPackageToPreview: BrewPackage = try foundPackageSelection!.getPackage(tracker: searchResultTracker)
openWindow(value: requestedPackageToPreview)
AppConstants.shared.logger.debug("Would preview package \(requestedPackageToPreview.name)")
}
catch {}
}
.disabled(foundPackageSelection == nil)
if isSearchFieldFocused
{
Button
{
packageInstallationProcessStep = .searching
} label: {
Text("add-package.search.action")
}
.keyboardShortcut(.defaultAction)
.disabled(packageRequested.isEmpty)
}
else
{
Button
{
getRequestedPackages()
packageInstallationProcessStep = .installing
} label: {
Text("add-package.install.action")
}
.keyboardShortcut(.defaultAction)
.disabled(foundPackageSelection == nil)
}
foundPackageSelection = nil // Clear all selected items when the user looks for a different package
}
}
.toolbar
{
ToolbarItem(placement: .primaryAction)
{
searchForPackageButton
}
ToolbarItem(placement: .primaryAction) {
startInstallProcessButton
}
ToolbarItemGroup(placement: .automatic)
{
previewPackageButton
startInstallProcessButton
}
}
}
@ViewBuilder
var previewPackageButton: some View
{
PreviewPackageButtonWithCustomAction
{
do
{
let requestedPackageToPreview: BrewPackage = try foundPackageSelection!.getPackage(tracker: searchResultTracker)
openWindow(value: requestedPackageToPreview)
AppConstants.shared.logger.debug("Would preview package \(requestedPackageToPreview.name)")
}
catch {}
}
.disabled(foundPackageSelection == nil)
}
@ViewBuilder
var searchForPackageButton: some View
{
Button
{
packageInstallationProcessStep = .searching
} label: {
Text("add-package.search.action")
}
.keyboardShortcut(.defaultAction)
.disabled(packageRequested.isEmpty || !isSearchFieldFocused)
}
@ViewBuilder
var startInstallProcessButton: some View
{
Button
{
getRequestedPackages()
packageInstallationProcessStep = .installing
} label: {
Text("add-package.install.action")
}
.keyboardShortcut(.defaultAction)
.disabled(foundPackageSelection == nil)
}
private func getRequestedPackages()
{
if let foundPackageSelection
@ -150,7 +168,7 @@ private struct SearchResultsSection: View
{
ForEach(packageList)
{ package in
SearchResultRow(searchedForPackage: package)
SearchResultRow(searchedForPackage: package, context: .searchResults, downloadCount: nil)
}
}
} header: {

View File

@ -18,27 +18,24 @@ struct InstallationSearchingView: View, Sendable
var body: some View
{
ProgressView("add-package.searching-\(packageRequested)")
.onAppear
.task
{
Task
searchResultTracker.foundFormulae = []
searchResultTracker.foundCasks = []
async let foundFormulae: [String] = searchForPackage(packageName: packageRequested, packageType: .formula)
async let foundCasks: [String] = searchForPackage(packageName: packageRequested, packageType: .cask)
for formula in await foundFormulae
{
searchResultTracker.foundFormulae = []
searchResultTracker.foundCasks = []
async let foundFormulae: [String] = try searchForPackage(packageName: packageRequested, packageType: .formula)
async let foundCasks: [String] = try searchForPackage(packageName: packageRequested, packageType: .cask)
for formula in try await foundFormulae
{
searchResultTracker.foundFormulae.append(BrewPackage(name: formula, type: .formula, installedOn: nil, versions: [], sizeInBytes: nil))
}
for cask in try await foundCasks
{
searchResultTracker.foundCasks.append(BrewPackage(name: cask, type: .cask, installedOn: nil, versions: [], sizeInBytes: nil))
}
packageInstallationProcessStep = .presentingSearchResults
searchResultTracker.foundFormulae.append(BrewPackage(name: formula, type: .formula, installedOn: nil, versions: [], sizeInBytes: nil))
}
for cask in await foundCasks
{
searchResultTracker.foundCasks.append(BrewPackage(name: cask, type: .cask, installedOn: nil, versions: [], sizeInBytes: nil))
}
packageInstallationProcessStep = .presentingSearchResults
}
}
}

View File

@ -0,0 +1,45 @@
//
// Another Process Already Running.swift
// Cork
//
// Created by David Bureš - P on 24.01.2025.
//
import SwiftUI
struct AnotherProcessAlreadyRunningView: View
{
@EnvironmentObject var appState: AppState
var body: some View
{
ComplexWithImage(image: Image(localURL: URL(string: "/System/Library/CoreServices/KeyboardSetupAssistant.app/Contents/Resources/AppIcon.icns")!)!)
{
VStack(alignment: .leading, spacing: 10)
{
Text("add-package.install.another-homebrew-process-blocking-install.title")
.font(.headline)
Text("add-package.install.another-homebrew-process-blocking-install.description")
}
.toolbar
{
ToolbarItem(placement: .destructiveAction)
{
Button("add-package.clear-brew-locks", role: .destructive)
{
if let contentsOfLockFolder = try? FileManager.default.contentsOfDirectory(at: URL(fileURLWithPath: "/usr/local/var/homebrew/locks"), includingPropertiesForKeys: [.isRegularFileKey])
{
for lockURL in contentsOfLockFolder
{
try? FileManager.default.removeItem(at: lockURL)
}
}
appState.dismissSheet()
}
}
}
}
}
}

View File

@ -12,6 +12,7 @@ struct BinaryAlreadyExistsView: View, Sendable
{
@Environment(\.dismiss) var dismiss: DismissAction
@EnvironmentObject var appState: AppState
@EnvironmentObject var brewData: BrewDataStorage
@ObservedObject var installationProgressTracker: InstallationProgressTracker
@ -27,10 +28,10 @@ struct BinaryAlreadyExistsView: View, Sendable
subheadline: "add-package.install.binary-already-exists.subheadline",
alignment: .leading
)
Spacer()
HStack
}
.toolbar
{
ToolbarItem(placement: .primaryAction)
{
Button
{
@ -38,21 +39,6 @@ struct BinaryAlreadyExistsView: View, Sendable
} label: {
Text("action.reveal-applications-folder-in-finder")
}
Spacer()
Button
{
dismiss()
Task.detached
{
await synchronizeInstalledPackages(brewData: brewData)
}
} label: {
Text("action.close")
}
.keyboardShortcut(.cancelAction)
}
}
}

View File

@ -0,0 +1,37 @@
//
// Installation Fatal Error.swift
// Cork
//
// Created by David Bureš - P on 24.01.2025.
//
import SwiftUI
struct InstallationFatalErrorView: View
{
@ObservedObject var installationProgressTracker: InstallationProgressTracker
var body: some View
{
ComplexWithIcon(systemName: "exclamationmark.triangle")
{
HeadlineWithSubheadline(
headline: "add-package.fatal-error-\(installationProgressTracker.packageBeingInstalled.package.name)",
subheadline: "add-package.fatal-error.description",
alignment: .leading
)
}
.toolbar
{
ToolbarItem(placement: .destructiveAction)
{
Button
{
restartApp()
} label: {
Text("action.restart")
}
}
}
}
}

View File

@ -0,0 +1,40 @@
//
// Installation Finished Successfully.swift
// Cork
//
// Created by David Bureš - P on 25.01.2025.
//
import CorkNotifications
import CorkShared
import SwiftUI
struct InstallationFinishedSuccessfullyView: View
{
@EnvironmentObject var cachedDownloadsTracker: CachedPackagesTracker
@AppStorage("notifyAboutPackageInstallationResults") var notifyAboutPackageInstallationResults: Bool = false
var body: some View
{
DisappearableSheet
{
ComplexWithIcon(systemName: "checkmark.seal")
{
HeadlineWithSubheadline(
headline: "add-package.finished",
subheadline: "add-package.finished.description",
alignment: .leading
)
}
}
.onAppear
{
if notifyAboutPackageInstallationResults
{
sendNotification(
title: String(localized: "notification.install-finished"))
}
}
}
}

View File

@ -24,26 +24,26 @@ struct InstallationTerminatedUnexpectedlyView: View
subheadline: "add-package.install.installation-terminated.subheadline",
alignment: .leading
)
DisclosureGroup
if usableLiveTerminalOutput.isEmpty
{
List
{
ForEach(usableLiveTerminalOutput)
{ outputLine in
Text(outputLine.line)
}
}
.frame(height: 100, alignment: .leading)
} label: {
Text("action.show-terminal-output")
OutlinedPillText(text: "add-package.install.installation-terminated.no-terminal-output-provided", color: .secondary)
}
HStack
else
{
Spacer()
DismissSheetButton()
DisclosureGroup
{
List
{
ForEach(usableLiveTerminalOutput)
{ outputLine in
Text(outputLine.line)
}
}
.frame(height: 100, alignment: .leading)
} label: {
Text("action.show-terminal-output")
}
}
}
}

View File

@ -11,6 +11,7 @@ struct SudoRequiredView: View, Sendable
{
@Environment(\.dismiss) var dismiss: DismissAction
@EnvironmentObject var appState: AppState
@EnvironmentObject var brewData: BrewDataStorage
@ObservedObject var installationProgressTracker: InstallationProgressTracker
@ -34,24 +35,11 @@ struct SudoRequiredView: View, Sendable
.font(.subheadline)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
HStack
}
.toolbar
{
ToolbarItem(placement: .primaryAction)
{
Button
{
dismiss()
Task.detached
{
await synchronizeInstalledPackages(brewData: brewData)
}
} label: {
Text("action.close")
}
.keyboardShortcut(.cancelAction)
Spacer()
Button
{
openTerminal()

View File

@ -11,6 +11,7 @@ struct WrongArchitectureView: View, Sendable
{
@Environment(\.dismiss) var dismiss: DismissAction
@EnvironmentObject var appState: AppState
@EnvironmentObject var brewData: BrewDataStorage
@ObservedObject var installationProgressTracker: InstallationProgressTracker
@ -26,24 +27,6 @@ struct WrongArchitectureView: View, Sendable
subheadline: "add-package.install.wrong-architecture-\(installationProgressTracker.packageBeingInstalled.package.name).user-architecture-is-\(ProcessInfo().CPUArchitecture == .arm ? "Apple Silicon" : "Intel")",
alignment: .leading
)
HStack
{
Spacer()
Button
{
dismiss()
Task.detached
{
await synchronizeInstalledPackages(brewData: brewData)
}
} label: {
Text("action.close")
}
.keyboardShortcut(.cancelAction)
}
}
}
.fixedSize()

View File

@ -7,6 +7,7 @@
import SwiftUI
import CorkShared
import ButtonKit
struct Licensing_NotBoughtOrActivatedView: View
{
@ -139,60 +140,57 @@ struct Licensing_NotBoughtOrActivatedView: View
.disabled(isDemoButtonDisabled)
}
Button
AsyncButton
{
Task(priority: .userInitiated)
withAnimation
{
withAnimation
{
isCheckingLicense = true
}
isCheckingLicense = true
}
defer
defer
{
DispatchQueue.main.asyncAfter(deadline: .now() + 3)
{
DispatchQueue.main.asyncAfter(deadline: .now() + 3)
withAnimation
{
withAnimation
{
isCheckingLicense = false
hasCheckingFailed = false
}
isCheckingLicense = false
hasCheckingFailed = false
}
}
}
do
do
{
let hasSpecifiedUserBoughtCork: Bool = try await checkIfUserBoughtCork(for: emailFieldContents)
AppConstants.shared.logger.debug("Has \(emailFieldContents) bought Cork? \(hasSpecifiedUserBoughtCork ? "YES" : "NO")")
if hasSpecifiedUserBoughtCork
{
let hasSpecifiedUserBoughtCork: Bool = try await checkIfUserBoughtCork(for: emailFieldContents)
AppConstants.shared.logger.debug("Has \(emailFieldContents) bought Cork? \(hasSpecifiedUserBoughtCork ? "YES" : "NO")")
if hasSpecifiedUserBoughtCork
appState.licensingState = .bought
}
else
{
withAnimation
{
appState.licensingState = .bought
}
else
{
withAnimation
{
hasCheckingFailed = true
}
hasCheckingFailed = true
}
}
catch let licenseCheckingError as CorkLicenseRetrievalError
{
AppConstants.shared.logger.error("\(licenseCheckingError.localizedDescription, privacy: .public)")
}
catch let licenseCheckingError as CorkLicenseRetrievalError
{
AppConstants.shared.logger.error("\(licenseCheckingError.localizedDescription, privacy: .public)")
switch licenseCheckingError
{
case .authorizationComplexNotEncodedProperly:
appState.showAlert(errorToShow: .licenseCheckingFailedDueToAuthorizationComplexNotBeingEncodedProperly)
case .notConnectedToTheInternet:
appState.showAlert(errorToShow: .licenseCheckingFailedDueToNoInternet)
case .operationTimedOut:
appState.showAlert(errorToShow: .licenseCheckingFailedDueToTimeout)
case .otherError(let localizedDescription):
appState.showAlert(errorToShow: .licenseCheckingFailedForOtherReason(localizedDescription: localizedDescription))
}
switch licenseCheckingError
{
case .authorizationComplexNotEncodedProperly:
appState.showAlert(errorToShow: .licenseCheckingFailedDueToAuthorizationComplexNotBeingEncodedProperly)
case .notConnectedToTheInternet:
appState.showAlert(errorToShow: .licenseCheckingFailedDueToNoInternet)
case .operationTimedOut:
appState.showAlert(errorToShow: .licenseCheckingFailedDueToTimeout)
case .otherError(let localizedDescription):
appState.showAlert(errorToShow: .licenseCheckingFailedForOtherReason(localizedDescription: localizedDescription))
}
}
} label: {
@ -200,6 +198,8 @@ struct Licensing_NotBoughtOrActivatedView: View
}
.keyboardShortcut(.defaultAction)
.disabled(emailFieldContents.isEmpty || !emailFieldContents.contains("@") || !emailFieldContents.contains("."))
.disabledWhenLoading()
.asyncButtonStyle(.none)
}
}
.padding()

View File

@ -14,6 +14,8 @@ enum MaintenanceSteps
struct MaintenanceView: View
{
@Environment(\.dismiss) var dismiss: DismissAction
@EnvironmentObject var brewData: BrewDataStorage
@EnvironmentObject var appState: AppState
@ -35,49 +37,107 @@ struct MaintenanceView: View
@State var reclaimedSpaceAfterCachePurge: Int = 0
@State var forcedOptions: Bool? = false
var isDismissable: Bool
{
[.ready, .finished].contains(maintenanceSteps)
}
var shouldShowTitle: Bool
{
[.ready].contains(maintenanceSteps)
}
var sheetTitle: LocalizedStringKey
{
switch maintenanceSteps
{
case .ready:
return "maintenance.title"
case .maintenanceRunning:
return ""
case .finished:
return ""
}
}
var dismissButtonTitle: LocalizedStringKey
{
switch maintenanceSteps {
case .ready:
return "action.cancel"
case .maintenanceRunning:
return ""
case .finished:
return "action.close"
}
}
var body: some View
{
VStack(alignment: .leading, spacing: 10)
NavigationStack
{
switch maintenanceSteps
SheetTemplate(isShowingTitle: shouldShowTitle)
{
case .ready:
MaintenanceReadyView(
shouldUninstallOrphans: $shouldUninstallOrphans,
shouldPurgeCache: $shouldPurgeCache,
shouldDeleteDownloads: $shouldDeleteDownloads,
shouldPerformHealthCheck: $shouldPerformHealthCheck,
maintenanceSteps: $maintenanceSteps,
isShowingControlButtons: true,
forcedOptions: forcedOptions!
)
Group
{
switch maintenanceSteps
{
case .ready:
MaintenanceReadyView(
shouldUninstallOrphans: $shouldUninstallOrphans,
shouldPurgeCache: $shouldPurgeCache,
shouldDeleteDownloads: $shouldDeleteDownloads,
shouldPerformHealthCheck: $shouldPerformHealthCheck,
maintenanceSteps: $maintenanceSteps,
isShowingControlButtons: true,
forcedOptions: forcedOptions!
)
case .maintenanceRunning:
MaintenanceRunningView(
shouldUninstallOrphans: shouldUninstallOrphans,
shouldPurgeCache: shouldPurgeCache,
shouldDeleteDownloads: shouldDeleteDownloads,
shouldPerformHealthCheck: shouldPerformHealthCheck,
numberOfOrphansRemoved: $numberOfOrphansRemoved,
packagesHoldingBackCachePurge: $packagesHoldingBackCachePurge,
reclaimedSpaceAfterCachePurge: $reclaimedSpaceAfterCachePurge,
brewHealthCheckFoundNoProblems: $brewHealthCheckFoundNoProblems,
maintenanceSteps: $maintenanceSteps
)
case .maintenanceRunning:
MaintenanceRunningView(
shouldUninstallOrphans: shouldUninstallOrphans,
shouldPurgeCache: shouldPurgeCache,
shouldDeleteDownloads: shouldDeleteDownloads,
shouldPerformHealthCheck: shouldPerformHealthCheck,
numberOfOrphansRemoved: $numberOfOrphansRemoved,
packagesHoldingBackCachePurge: $packagesHoldingBackCachePurge,
reclaimedSpaceAfterCachePurge: $reclaimedSpaceAfterCachePurge,
brewHealthCheckFoundNoProblems: $brewHealthCheckFoundNoProblems,
maintenanceSteps: $maintenanceSteps
)
case .finished:
MaintenanceFinishedView(
shouldUninstallOrphans: shouldUninstallOrphans,
shouldPurgeCache: shouldPurgeCache,
shouldDeleteDownloads: shouldDeleteDownloads,
shouldPerformHealthCheck: shouldPerformHealthCheck,
packagesHoldingBackCachePurge: packagesHoldingBackCachePurge,
numberOfOrphansRemoved: numberOfOrphansRemoved,
reclaimedSpaceAfterCachePurge: reclaimedSpaceAfterCachePurge,
brewHealthCheckFoundNoProblems: brewHealthCheckFoundNoProblems,
maintenanceFoundNoProblems: $maintenanceFoundNoProblems
)
case .finished:
MaintenanceFinishedView(
shouldUninstallOrphans: shouldUninstallOrphans,
shouldPurgeCache: shouldPurgeCache,
shouldDeleteDownloads: shouldDeleteDownloads,
shouldPerformHealthCheck: shouldPerformHealthCheck,
packagesHoldingBackCachePurge: packagesHoldingBackCachePurge,
numberOfOrphansRemoved: numberOfOrphansRemoved,
reclaimedSpaceAfterCachePurge: reclaimedSpaceAfterCachePurge,
brewHealthCheckFoundNoProblems: brewHealthCheckFoundNoProblems,
maintenanceFoundNoProblems: $maintenanceFoundNoProblems
)
}
}
.navigationTitle(sheetTitle)
.toolbar
{
if isDismissable
{
ToolbarItem(placement: .cancellationAction)
{
Button
{
dismiss()
} label: {
Text(dismissButtonTitle)
}
.keyboardShortcut(.cancelAction)
}
}
}
}
}
}

View File

@ -5,8 +5,8 @@
// Created by David Bureš on 04.10.2023.
//
import SwiftUI
import CorkShared
import SwiftUI
struct MaintenanceFinishedView: View
{
@ -16,6 +16,9 @@ struct MaintenanceFinishedView: View
@EnvironmentObject var appState: AppState
@EnvironmentObject var brewData: BrewDataStorage
@EnvironmentObject var cachedDownloadsTracker: CachedPackagesTracker
@EnvironmentObject var outdatedPackageTacker: OutdatedPackageTracker
let shouldUninstallOrphans: Bool
@ -67,120 +70,102 @@ struct MaintenanceFinishedView: View
{
ComplexWithIcon(systemName: "checkmark.seal")
{
VStack(alignment: .center)
VStack(alignment: .leading, spacing: 5)
{
VStack(alignment: .leading, spacing: 5)
Text("maintenance.finished")
.font(.headline)
if shouldUninstallOrphans
{
Text("maintenance.finished")
.font(.headline)
Text("maintenance.results.orphans-count-\(numberOfOrphansRemoved)")
}
if shouldUninstallOrphans
if shouldPurgeCache
{
VStack(alignment: .leading)
{
Text("maintenance.results.orphans-count-\(numberOfOrphansRemoved)")
}
Text("maintenance.results.package-cache")
if shouldPurgeCache
{
VStack(alignment: .leading)
if !displayablePackagesHoldingBackCachePurge.isEmpty
{
Text("maintenance.results.package-cache")
if !displayablePackagesHoldingBackCachePurge.isEmpty
if displayablePackagesHoldingBackCachePurge.count >= 3
{
if displayablePackagesHoldingBackCachePurge.count >= 3
{
let packageNamesNotTruncated: [String] = Array(displayablePackagesHoldingBackCachePurge.prefix(3))
let packageNamesNotTruncated: [String] = Array(displayablePackagesHoldingBackCachePurge.prefix(3))
let numberOfTruncatedPackages: Int = displayablePackagesHoldingBackCachePurge.count - packageNamesNotTruncated.count
let numberOfTruncatedPackages: Int = displayablePackagesHoldingBackCachePurge.count - packageNamesNotTruncated.count
Text("maintenance.results.package-cache.skipped-\(packageNamesNotTruncated.formatted(.list(type: .and)))-and-\(numberOfTruncatedPackages)-others")
.font(.caption)
.foregroundColor(Color(nsColor: NSColor.systemGray))
}
else
{
Text("maintenance.results.package-cache.skipped-\(displayablePackagesHoldingBackCachePurge.formatted(.list(type: .and)))")
.font(.caption)
.foregroundColor(Color(nsColor: NSColor.systemGray))
}
Text("maintenance.results.package-cache.skipped-\(packageNamesNotTruncated.formatted(.list(type: .and)))-and-\(numberOfTruncatedPackages)-others")
.font(.caption)
.foregroundColor(Color(nsColor: NSColor.systemGray))
}
else
{
Text("maintenance.results.package-cache.skipped-\(displayablePackagesHoldingBackCachePurge.formatted(.list(type: .and)))")
.font(.caption)
.foregroundColor(Color(nsColor: NSColor.systemGray))
}
/*
if cachePurgingSkippedPackagesDueToMostRecentVersionsNotBeingInstalled
{
if packagesHoldingBackCachePurgeTracker.count > 2
{
Text("maintenance.results.package-cache.skipped-\(packagesHoldingBackCachePurgeTracker[0...1].joined(separator: ", "))-and-\(packagesHoldingBackCachePurgeTracker.count - 2)-others")
.font(.caption)
.foregroundColor(Color(nsColor: NSColor.systemGray))
}
else
{
Text("maintenance.results.package-cache.skipped-\(packagesHoldingBackCachePurgeTracker.joined(separator: ", "))")
.font(.caption)
.foregroundColor(Color(nsColor: NSColor.systemGray))
}
}
*/
}
}
if shouldDeleteDownloads
{
VStack(alignment: .leading)
{
Text("maintenance.results.cached-downloads")
Text("maintenance.results.cached-downloads.summary-\(reclaimedSpaceAfterCachePurge.formatted(.byteCount(style: .file)))")
.font(.caption)
.foregroundColor(Color(nsColor: NSColor.systemGray))
}
}
/*
if cachePurgingSkippedPackagesDueToMostRecentVersionsNotBeingInstalled
{
if packagesHoldingBackCachePurgeTracker.count > 2
{
if shouldPerformHealthCheck
{
if brewHealthCheckFoundNoProblems
{
Text("maintenance.results.health-check.problems-none")
}
else
{
Text("maintenance.results.health-check.problems")
.onAppear
{
maintenanceFoundNoProblems = false
}
}
Text("maintenance.results.package-cache.skipped-\(packagesHoldingBackCachePurgeTracker[0...1].joined(separator: ", "))-and-\(packagesHoldingBackCachePurgeTracker.count - 2)-others")
.font(.caption)
.foregroundColor(Color(nsColor: NSColor.systemGray))
}
else
{
Text("maintenance.results.package-cache.skipped-\(packagesHoldingBackCachePurgeTracker.joined(separator: ", "))")
.font(.caption)
.foregroundColor(Color(nsColor: NSColor.systemGray))
}
}
*/
}
}
Spacer()
HStack
if shouldDeleteDownloads
{
Spacer()
Button
VStack(alignment: .leading)
{
dismiss()
appState.cachedDownloadsFolderSize = AppConstants.shared.brewCachedDownloadsPath.directorySize
} label: {
Text("action.close")
Text("maintenance.results.cached-downloads")
Text("maintenance.results.cached-downloads.summary-\(reclaimedSpaceAfterCachePurge.formatted(.byteCount(style: .file)))")
.font(.caption)
.foregroundColor(Color(nsColor: NSColor.systemGray))
}
}
if shouldPerformHealthCheck
{
if brewHealthCheckFoundNoProblems
{
Text("maintenance.results.health-check.problems-none")
}
else
{
Text("maintenance.results.health-check.problems")
.onAppear
{
maintenanceFoundNoProblems = false
}
}
.keyboardShortcut(.defaultAction)
}
}
.fixedSize()
}
.padding()
// .frame(minWidth: 300, minHeight: 150)
.onAppear // This should stay this way, I don' want the task to be cancelled when the view disappears
.task
{
Task
do
{
await synchronizeInstalledPackages(brewData: brewData)
try await brewData.synchronizeInstalledPackages(cachedPackagesTracker: cachedDownloadsTracker)
}
catch let synchronizationError
{
appState.showAlert(errorToShow: .couldNotSynchronizePackages(error: synchronizationError.localizedDescription))
}
}
}

View File

@ -13,6 +13,8 @@ struct MaintenanceRunningView: View
@EnvironmentObject var appState: AppState
@EnvironmentObject var brewData: BrewDataStorage
@EnvironmentObject var cachedDownloadsTracker: CachedPackagesTracker
@State var currentMaintenanceStepText: LocalizedStringKey = "maintenance.step.initial"
let shouldUninstallOrphans: Bool
@ -31,7 +33,7 @@ struct MaintenanceRunningView: View
ProgressView
{
Text(currentMaintenanceStepText)
.task(priority: .userInitiated)
.task
{
if shouldUninstallOrphans
{
@ -77,13 +79,28 @@ struct MaintenanceRunningView: View
currentMaintenanceStepText = "maintenance.step.deleting-cached-downloads"
deleteCachedDownloads()
do throws(CachedDownloadDeletionError)
{
try deleteCachedDownloads()
}
catch let cacheDeletionError
{
switch cacheDeletionError
{
case .couldNotReadContentsOfCachedFormulaeDownloadsFolder(let associatedError):
appState.showAlert(errorToShow: .couldNotDeleteCachedDownloads(error: associatedError))
case .couldNotReadContentsOfCachedCasksDownloadsFolder(let associatedError):
appState.showAlert(errorToShow: .couldNotDeleteCachedDownloads(error: associatedError))
case .couldNotReadContentsOfCachedDownloadsFolder(let associatedError):
appState.showAlert(errorToShow: .couldNotDeleteCachedDownloads(error: associatedError))
}
}
/// I have to assign the original value of the appState variable to a different variable, because when it updates at the end of the process, I don't want it to update in the result overview
reclaimedSpaceAfterCachePurge = Int(appState.cachedDownloadsFolderSize)
reclaimedSpaceAfterCachePurge = Int(cachedDownloadsTracker.cachedDownloadsSize)
await appState.loadCachedDownloadedPackages()
appState.assignPackageTypeToCachedDownloads(brewData: brewData)
}
else
{
@ -114,7 +131,5 @@ struct MaintenanceRunningView: View
maintenanceSteps = .finished
}
}
.padding()
.frame(width: 200)
}
}

View File

@ -5,8 +5,8 @@
// Created by David Bureš on 25.02.2023.
//
import SwiftUI
import CorkShared
import SwiftUI
struct MaintenanceReadyView: View
{
@ -30,91 +30,81 @@ struct MaintenanceReadyView: View
var body: some View
{
SheetWithTitle(title: "maintenance.title")
VStack(alignment: .leading, spacing: 10)
{
VStack(alignment: .leading, spacing: 10)
Form
{
Form
LabeledContent("maintenance.steps.packages")
{
LabeledContent("maintenance.steps.packages")
VStack(alignment: .leading)
{
VStack(alignment: .leading)
Toggle(isOn: $shouldUninstallOrphans)
{
Toggle(isOn: $shouldUninstallOrphans)
{
Text("maintenance.steps.packages.uninstall-orphans")
}
}
}
LabeledContent("maintenance.steps.downloads")
{
VStack(alignment: .leading)
{
Toggle(isOn: $shouldPurgeCache)
{
Text("maintenance.steps.downloads.purge-cache")
}
Toggle(isOn: $shouldDeleteDownloads)
{
Text("maintenance.steps.downloads.delete-cached-downloads")
}
}
}
LabeledContent("maintenance.steps.other")
{
Toggle(isOn: $shouldPerformHealthCheck)
{
Text("maintenance.steps.other.health-check")
Text("maintenance.steps.packages.uninstall-orphans")
}
}
}
if isShowingControlButtons
LabeledContent("maintenance.steps.downloads")
{
HStack
VStack(alignment: .leading)
{
DismissSheetButton()
Spacer()
Button
Toggle(isOn: $shouldPurgeCache)
{
AppConstants.shared.logger.debug("Start")
maintenanceSteps = .maintenanceRunning
} label: {
Text("maintenance.steps.start")
Text("maintenance.steps.downloads.purge-cache")
}
Toggle(isOn: $shouldDeleteDownloads)
{
Text("maintenance.steps.downloads.delete-cached-downloads")
}
.keyboardShortcut(.defaultAction)
.disabled(isStartDisabled)
}
// .padding(.top)
}
}
.onAppear
{
if !forcedOptions
LabeledContent("maintenance.steps.other")
{
/// Replace the provided values with those from AppStorage
/// I have to do this because I don't want the settings in the sheet itself to affect those in the defaults
shouldUninstallOrphans = default_shouldUninstallOrphans
shouldPurgeCache = default_shouldPurgeCache
shouldDeleteDownloads = default_shouldDeleteDownloads
shouldPerformHealthCheck = default_shouldPerformHealthCheck
Toggle(isOn: $shouldPerformHealthCheck)
{
Text("maintenance.steps.other.health-check")
}
}
}
}
.if(enablePadding, transform: { viewProxy in
viewProxy
.padding()
})
.toolbar
{
if isShowingControlButtons
{
ToolbarItem(placement: .primaryAction)
{
Button
{
AppConstants.shared.logger.debug("Start")
maintenanceSteps = .maintenanceRunning
} label: {
Text("maintenance.steps.start")
}
.keyboardShortcut(.defaultAction)
.disabled(isStartDisabled)
}
}
}
.onAppear
{
if !forcedOptions
{
/// Replace the provided values with those from AppStorage
/// I have to do this because I don't want the settings in the sheet itself to affect those in the defaults
shouldUninstallOrphans = default_shouldUninstallOrphans
shouldPurgeCache = default_shouldPurgeCache
shouldDeleteDownloads = default_shouldDeleteDownloads
shouldPerformHealthCheck = default_shouldPerformHealthCheck
}
}
}
private var isStartDisabled: Bool
{
[shouldUninstallOrphans, shouldPurgeCache, shouldDeleteDownloads, shouldPerformHealthCheck].allSatisfy
[shouldUninstallOrphans, shouldPurgeCache, shouldDeleteDownloads, shouldPerformHealthCheck].allSatisfy
{
!$0
}

View File

@ -14,7 +14,7 @@ struct MenuBarItem: View
@EnvironmentObject var appState: AppState
@EnvironmentObject var brewData: BrewDataStorage
@EnvironmentObject var availableTaps: AvailableTaps
@EnvironmentObject var availableTaps: TapTracker
@EnvironmentObject var outdatedPackageTracker: OutdatedPackageTracker

View File

@ -8,6 +8,7 @@
import SwiftUI
import CorkShared
import CorkNotifications
import ButtonKit
struct MenuBar_CacheCleanup: View
{
@ -17,50 +18,49 @@ struct MenuBar_CacheCleanup: View
{
if !isPurgingHomebrewCache
{
Button("maintenance.steps.downloads.purge-cache")
AsyncButton
{
Task(priority: .userInitiated)
AppConstants.shared.logger.log("Will purge cache")
isPurgingHomebrewCache = true
defer
{
AppConstants.shared.logger.log("Will purge cache")
isPurgingHomebrewCache = false
}
isPurgingHomebrewCache = true
do
{
let packagesHoldingBackCachePurge: [String] = try await purgeHomebrewCacheUtility()
defer
if packagesHoldingBackCachePurge.isEmpty
{
isPurgingHomebrewCache = false
}
do
{
let packagesHoldingBackCachePurge: [String] = try await purgeHomebrewCacheUtility()
if packagesHoldingBackCachePurge.isEmpty
{
sendNotification(
title: String(localized: "maintenance.results.package-cache"),
sensitivity: .active
)
}
else
{
sendNotification(
title: String(localized: "maintenance.results.package-cache"),
body: String(localized: "maintenance.results.package-cache.skipped-\(packagesHoldingBackCachePurge.formatted(.list(type: .and)))"),
sensitivity: .active
)
}
}
catch let cachePurgingError
{
AppConstants.shared.logger.warning("There were errors while purging Homebrew cache: \(cachePurgingError.localizedDescription, privacy: .public)")
sendNotification(
title: String(localized: "maintenance.results.package-cache.failure"),
body: String(localized: "maintenance.results.package-cache.failure.details-\(cachePurgingError.localizedDescription)"),
title: String(localized: "maintenance.results.package-cache"),
sensitivity: .active
)
}
else
{
sendNotification(
title: String(localized: "maintenance.results.package-cache"),
body: String(localized: "maintenance.results.package-cache.skipped-\(packagesHoldingBackCachePurge.formatted(.list(type: .and)))"),
sensitivity: .active
)
}
}
catch let cachePurgingError
{
AppConstants.shared.logger.warning("There were errors while purging Homebrew cache: \(cachePurgingError.localizedDescription, privacy: .public)")
sendNotification(
title: String(localized: "maintenance.results.package-cache.failure"),
body: String(localized: "maintenance.results.package-cache.failure.details-\(cachePurgingError.localizedDescription)"),
sensitivity: .active
)
}
} label: {
Text("maintenance.steps.downloads.purge-cache")
}
}
else

View File

@ -13,21 +13,40 @@ struct MenuBar_CachedDownloadsCleanup: View
{
@EnvironmentObject var appState: AppState
@EnvironmentObject var cachedDownloadsTracker: CachedPackagesTracker
@State private var isDeletingCachedDownloads: Bool = false
var body: some View
{
if !isDeletingCachedDownloads
{
Button(appState.cachedDownloadsFolderSize != 0 ? "maintenance.steps.downloads.delete-cached-downloads" : "navigation.menu.maintenance.no-cached-downloads")
Button(cachedDownloadsTracker.cachedDownloadsSize != 0 ? "maintenance.steps.downloads.delete-cached-downloads" : "navigation.menu.maintenance.no-cached-downloads")
{
AppConstants.shared.logger.log("Will delete cached downloads")
isDeletingCachedDownloads = true
let reclaimedSpaceAfterCachePurge: Int = .init(appState.cachedDownloadsFolderSize)
let reclaimedSpaceAfterCachePurge: Int = .init(cachedDownloadsTracker.cachedDownloadsSize)
deleteCachedDownloads()
do throws(CachedDownloadDeletionError)
{
try deleteCachedDownloads()
}
catch let cacheDeletionError
{
switch cacheDeletionError
{
case .couldNotReadContentsOfCachedFormulaeDownloadsFolder(let associatedError):
appState.showAlert(errorToShow: .couldNotDeleteCachedDownloads(error: associatedError))
case .couldNotReadContentsOfCachedCasksDownloadsFolder(let associatedError):
appState.showAlert(errorToShow: .couldNotDeleteCachedDownloads(error: associatedError))
case .couldNotReadContentsOfCachedDownloadsFolder(let associatedError):
appState.showAlert(errorToShow: .couldNotDeleteCachedDownloads(error: associatedError))
}
}
sendNotification(
title: String(localized: "maintenance.results.cached-downloads"),
@ -36,10 +55,8 @@ struct MenuBar_CachedDownloadsCleanup: View
)
isDeletingCachedDownloads = false
appState.cachedDownloadsFolderSize = AppConstants.shared.brewCachedDownloadsPath.directorySize
}
.disabled(appState.cachedDownloadsFolderSize == 0)
.disabled(cachedDownloadsTracker.cachedDownloadsSize == 0)
}
else
{

View File

@ -5,13 +5,16 @@
// Created by David Bureš on 30.03.2024.
//
import SwiftUI
import CorkShared
import ButtonKit
import CorkNotifications
import CorkShared
import SwiftUI
struct MenuBar_OrphanCleanup: View
{
@EnvironmentObject var appState: AppState
@EnvironmentObject var brewData: BrewDataStorage
@EnvironmentObject var cachedPackagesTracker: CachedPackagesTracker
@State private var isUninstallingOrphanedPackages: Bool = false
@ -19,35 +22,41 @@ struct MenuBar_OrphanCleanup: View
{
if !isUninstallingOrphanedPackages
{
Button("maintenance.steps.packages.uninstall-orphans")
AsyncButton
{
Task(priority: .userInitiated)
AppConstants.shared.logger.log("Will delete orphans")
do
{
AppConstants.shared.logger.log("Will delete orphans")
let orphanUninstallResult: Int = try await uninstallOrphansUtility()
do
{
let orphanUninstallResult: Int = try await uninstallOrphansUtility()
sendNotification(
title: String(localized: "maintenance.results.orphans-removed"),
body: String(localized: "maintenance.results.orphans-count-\(orphanUninstallResult)"),
sensitivity: .active
)
}
catch let orphanUninstallationError
{
AppConstants.shared.logger.error("Failed while uninstalling orphans: \(orphanUninstallationError, privacy: .public)")
sendNotification(
title: String(localized: "maintenance.results.orphans.failure"),
body: String(localized: "maintenance.results.orphans.failure.details-\(orphanUninstallationError.localizedDescription)"),
sensitivity: .active
)
}
await synchronizeInstalledPackages(brewData: brewData)
sendNotification(
title: String(localized: "maintenance.results.orphans-removed"),
body: String(localized: "maintenance.results.orphans-count-\(orphanUninstallResult)"),
sensitivity: .active
)
}
catch let orphanUninstallationError
{
AppConstants.shared.logger.error("Failed while uninstalling orphans: \(orphanUninstallationError, privacy: .public)")
sendNotification(
title: String(localized: "maintenance.results.orphans.failure"),
body: String(localized: "maintenance.results.orphans.failure.details-\(orphanUninstallationError.localizedDescription)"),
sensitivity: .active
)
}
do
{
try await brewData.synchronizeInstalledPackages(cachedPackagesTracker: cachedPackagesTracker)
}
catch let synchronizationError
{
appState.showAlert(errorToShow: .couldNotSynchronizePackages(error: synchronizationError.localizedDescription))
}
} label: {
Text("maintenance.steps.packages.uninstall-orphans")
}
}
else

View File

@ -19,7 +19,7 @@ struct MenuBar_PackageInstallation: View
{
openWindow(id: "main")
switchCorkToForeground()
appState.isShowingInstallationSheet.toggle()
appState.showSheet(ofType: .packageInstallation)
}
}
}

View File

@ -10,7 +10,7 @@ import SwiftUI
struct MenuBar_PackageOverview: View
{
@EnvironmentObject var brewData: BrewDataStorage
@EnvironmentObject var availableTaps: AvailableTaps
@EnvironmentObject var availableTaps: TapTracker
var body: some View
{

View File

@ -23,22 +23,29 @@ struct MenuBar_PackageUpdating: View
{
if !outdatedPackageTracker.displayableOutdatedPackages.isEmpty
{
if !appState.isShowingUpdateSheet
if let sanitizedSheetState = appState.sheetToShow
{
Menu
if sanitizedSheetState != .fullUpdate || sanitizedSheetState != .partialUpdate
{
ForEach(outdatedPackageTracker.displayableOutdatedPackages.sorted(by: { $0.package.installedOn! < $1.package.installedOn! }))
{ outdatedPackage in
SanitizedPackageName(packageName: outdatedPackage.package.name, shouldShowVersion: false)
Menu
{
ForEach(outdatedPackageTracker.displayableOutdatedPackages.sorted(by: { $0.package.installedOn! < $1.package.installedOn! }))
{ outdatedPackage in
SanitizedPackageName(packageName: outdatedPackage.package.name, shouldShowVersion: false)
}
} label: {
Text("notification.outdated-packages-found.body-\(outdatedPackageTracker.displayableOutdatedPackages.count)")
}
Button("navigation.upgrade-packages")
{
switchCorkToForeground()
appState.showSheet(ofType: .fullUpdate)
}
} label: {
Text("notification.outdated-packages-found.body-\(outdatedPackageTracker.displayableOutdatedPackages.count)")
}
Button("navigation.upgrade-packages")
else
{
switchCorkToForeground()
appState.isShowingUpdateSheet = true
Text("update-packages.detail-stage.pouring")
}
}
else

View File

@ -106,7 +106,7 @@ struct PackageDetailView: View, Sendable
}
}
.frame(minWidth: 450, minHeight: 400, alignment: .topLeading)
.task(id: package.id, priority: .userInitiated)
.task(id: package.id)
{
isLoadingDetails = true
defer

View File

@ -6,9 +6,17 @@
//
import SwiftUI
import CorkShared
struct PackageDetailHeaderComplex: View
{
enum PackageDependantsDisplayStage: Equatable
{
case loadingDependants, showingDependants(dependantsToShow: [String]), noDependantsToShow
}
@EnvironmentObject var appState: AppState
let package: BrewPackage
var isInPreviewWindow: Bool
@ -16,6 +24,47 @@ struct PackageDetailHeaderComplex: View
@ObservedObject var packageDetails: BrewPackageDetails
let isLoadingDetails: Bool
@Namespace var packageDependantsAnimationNamespace
/// Controls whether the pill for showing dependants is shown
var packageDependantsDisplayStage: PackageDependantsDisplayStage
{
if packageDetails.installedAsDependency
{
if let dependants = packageDetails.dependents
{
if dependants.isEmpty // This happens when the package was originally installed as a dependency, but the parent is no longer installed
{
return .noDependantsToShow
}
else
{
return .showingDependants(dependantsToShow: dependants)
}
}
else
{
return .loadingDependants
}
}
else
{
return .noDependantsToShow
}
}
var packageDependantsPillColor: Color
{
switch self.packageDependantsDisplayStage {
case .loadingDependants:
return .init(nsColor: NSColor.tertiaryLabelColor)
case .showingDependants:
return .secondary
case .noDependantsToShow:
return .clear
}
}
var body: some View
{
@ -46,36 +95,15 @@ struct PackageDetailHeaderComplex: View
{
if !isInPreviewWindow
{
if packageDetails.installedAsDependency
{
if let packageDependents = packageDetails.dependents
{
if !packageDependents.isEmpty // This happens when the package was originally installed as a dependency, but the parent is no longer installed
{
OutlinedPillText(text: "package-details.dependants.dependency-of-\(packageDependents.formatted(.list(type: .and)))", color: .secondary)
}
}
else
{
OutlinedPill(content: {
HStack(alignment: .center, spacing: 5)
{
ProgressView()
.controlSize(.mini)
Text("package-details.dependants.loading")
}
}, color: Color(nsColor: NSColor.tertiaryLabelColor))
}
}
if packageDetails.outdated
{
OutlinedPillText(text: "package-details.outdated", color: .orange)
}
dependantsPill
packageDetailsPill
}
PackageCaveatMinifiedDisplayView(caveats: packageDetails.caveats)
}
.animation(appState.enableExtraAnimations ? .interpolatingSpring : .none, value: packageDependantsDisplayStage)
if !isLoadingDetails
{
@ -92,4 +120,41 @@ struct PackageDetailHeaderComplex: View
}
}
}
@ViewBuilder
var dependantsPill: some View
{
if packageDetails.installedAsDependency
{
OutlinedPill(content: {
switch packageDependantsDisplayStage
{
case .loadingDependants:
HStack(alignment: .center, spacing: 5)
{
ProgressView()
.controlSize(.mini)
Text("package-details.dependants.loading")
.matchedGeometryEffect(id: "dependantsPillContents", in: packageDependantsAnimationNamespace)
}
case .showingDependants(let dependantsToShow):
Text("package-details.dependants.dependency-of-\(dependantsToShow.formatted(.list(type: .and)))")
.matchedGeometryEffect(id: "dependantsPillContents", in: packageDependantsAnimationNamespace)
case .noDependantsToShow:
EmptyView()
}
}, color: packageDependantsPillColor)
}
}
@ViewBuilder
var packageDetailsPill: some View
{
if packageDetails.outdated
{
OutlinedPillText(text: "package-details.outdated", color: .orange)
}
}
}

View File

@ -7,6 +7,7 @@
import SwiftUI
import CorkShared
import ButtonKit
struct PackageModificationButtons: View
{
@ -15,14 +16,13 @@ struct PackageModificationButtons: View
@EnvironmentObject var brewData: BrewDataStorage
@EnvironmentObject var appState: AppState
@EnvironmentObject var cachedPackagesTracker: CachedPackagesTracker
@EnvironmentObject var outdatedPackageTracker: OutdatedPackageTracker
@EnvironmentObject var uninstallationConfirmationTracker: UninstallationConfirmationTracker
let package: BrewPackage
@ObservedObject var packageDetails: BrewPackageDetails
@State private var isPinning: Bool = false
let isLoadingDetails: Bool
var body: some View
@ -35,45 +35,21 @@ struct PackageModificationButtons: View
{
if package.type == .formula
{
Button
AsyncButton
{
Task
do
{
withAnimation
{
isPinning = true
}
defer
{
withAnimation
{
isPinning = false
}
}
do
{
try await packageDetails.changePinnedStatus()
}
catch let pinningUnpinningError
{
AppConstants.shared.logger.error("Failed while pinning/unpinning package \(package.name): \(pinningUnpinningError)")
}
try await packageDetails.changePinnedStatus()
}
catch let pinningUnpinningError
{
AppConstants.shared.logger.error("Failed while pinning/unpinning package \(package.name): \(pinningUnpinningError)")
}
} label: {
HStack(alignment: .center, spacing: 5)
{
if isPinning
{
ProgressView()
.controlSize(.mini)
.transition(.move(edge: .leading).combined(with: .opacity))
}
Text(packageDetails.pinned ? "package-details.action.unpin-version-\(package.versions.formatted(.list(type: .and)))" : "package-details.action.pin-version-\(package.versions.formatted(.list(type: .and)))")
}
Text(packageDetails.pinned ? "package-details.action.unpin-version-\(package.versions.formatted(.list(type: .and)))" : "package-details.action.pin-version-\(package.versions.formatted(.list(type: .and)))")
}
.disabled(isPinning)
.asyncButtonStyle(.leading)
.disabledWhenLoading()
}
Spacer()
@ -108,6 +84,7 @@ struct PackageModificationButtons: View
try await brewData.uninstallSelectedPackage(
package: package,
cachedPackagesTracker: cachedPackagesTracker,
appState: appState,
outdatedPackageTracker: outdatedPackageTracker,
shouldRemoveAllAssociatedFiles: false,
@ -122,7 +99,6 @@ struct PackageModificationButtons: View
}
}
.fixedSize()
.disabled(isPinning)
}
}
}

View File

@ -11,8 +11,11 @@ import CorkShared
struct ReinstallCorruptedPackageView: View
{
@Environment(\.dismiss) var dismiss: DismissAction
@EnvironmentObject var appState: AppState
@EnvironmentObject var brewData: BrewDataStorage
@EnvironmentObject var cachedPackagesTracker: CachedPackagesTracker
let corruptedPackageToReinstall: CorruptedPackage
@ -20,41 +23,69 @@ struct ReinstallCorruptedPackageView: View
var body: some View
{
switch corruptedPackageReinstallationStage
NavigationStack
{
case .installing:
ProgressView
switch corruptedPackageReinstallationStage
{
Text("repair-package.repair-process-\(corruptedPackageToReinstall.name)")
}
.progressViewStyle(.linear)
.padding()
.task(priority: .userInitiated)
{
let reinstallationResult: TerminalOutput = await shell(AppConstants.shared.brewExecutablePath, ["reinstall", corruptedPackageToReinstall.name])
AppConstants.shared.logger.debug("Reinstallation result:\nStandard output: \(reinstallationResult.standardOutput, privacy: .public)\nStandard error:\(reinstallationResult.standardError, privacy: .public)")
corruptedPackageReinstallationStage = .finished
}
case .finished:
DisappearableSheet
{
ComplexWithIcon(systemName: "checkmark.seal")
case .installing:
ProgressView
{
HeadlineWithSubheadline(
headline: "repair-package.repairing-finished.headline-\(corruptedPackageToReinstall.name)",
subheadline: "repair-package.repairing-finished.subheadline",
alignment: .leading
)
VStack(alignment: .leading)
{
Text("repair-package.repair-process-\(corruptedPackageToReinstall.name)")
SubtitleText(text: "repair-package.repair-length.explanation")
}
}
.task(priority: .background)
.progressViewStyle(.linear)
.padding()
.toolbar
{
await synchronizeInstalledPackages(brewData: brewData)
ToolbarItem(placement: .cancellationAction)
{
Button
{
dismiss()
} label: {
Text("action.cancel")
}
}
}
.task
{
let reinstallationResult: TerminalOutput = await shell(AppConstants.shared.brewExecutablePath, ["reinstall", corruptedPackageToReinstall.name])
AppConstants.shared.logger.debug("Reinstallation result:\nStandard output: \(reinstallationResult.standardOutput, privacy: .public)\nStandard error:\(reinstallationResult.standardError, privacy: .public)")
corruptedPackageReinstallationStage = .finished
}
case .finished:
DisappearableSheet
{
ComplexWithIcon(systemName: "checkmark.seal")
{
HeadlineWithSubheadline(
headline: "repair-package.repairing-finished.headline-\(corruptedPackageToReinstall.name)",
subheadline: "repair-package.repairing-finished.subheadline",
alignment: .leading
)
}
.task
{
do
{
try await brewData.synchronizeInstalledPackages(cachedPackagesTracker: cachedPackagesTracker)
}
catch let synchronizationError
{
appState.showAlert(errorToShow: .couldNotSynchronizePackages(error: synchronizationError.localizedDescription))
}
}
}
.padding()
.fixedSize()
}
.padding()
.fixedSize()
}
}
}

View File

@ -6,6 +6,7 @@
//
import SwiftUI
import ButtonKit
struct SudoRequiredForRemovalSheet: View, Sendable
{
@ -13,6 +14,7 @@ struct SudoRequiredForRemovalSheet: View, Sendable
@EnvironmentObject var brewData: BrewDataStorage
@EnvironmentObject var appState: AppState
@EnvironmentObject var cachedPackagesTracker: CachedPackagesTracker
var body: some View
{
@ -38,18 +40,23 @@ struct SudoRequiredForRemovalSheet: View, Sendable
HStack
{
Button
AsyncButton
{
dismiss()
Task.detached
do
{
await synchronizeInstalledPackages(brewData: brewData)
try await brewData.synchronizeInstalledPackages(cachedPackagesTracker: cachedPackagesTracker)
}
catch let synchronizationError
{
appState.showAlert(errorToShow: .couldNotSynchronizePackages(error: synchronizationError.localizedDescription))
}
} label: {
Text("action.close")
}
.keyboardShortcut(.cancelAction)
.asyncButtonStyle(.plainStyle)
Spacer()

View File

@ -0,0 +1,16 @@
//
// Reinstall Homebrew.swift
// Cork
//
// Created by David Bureš - P on 18.01.2025.
//
import SwiftUI
struct ReinstallHomebrewButton: View
{
var body: some View
{
ButtonThatOpensWebsites(websiteURL: URL(string: "https://github.com/homebrew/install?tab=readme-ov-file#uninstall-homebrew")!, buttonText: "action.reinstall-homebrew")
}
}

View File

@ -110,3 +110,25 @@ struct GroupBoxHeadlineGroupWithArbitraryContent<Content: View>: View
.padding(2)
}
}
struct GroupBoxHeadlineGroupWithArbitraryImageAndContent<Content: View>: View
{
var imageName: String?
@ViewBuilder var content: Content
var body: some View {
HStack(alignment: .top, spacing: 15)
{
if let imageName
{
Image(imageName)
.resizable()
.scaledToFit()
.frame(width: 26, height: 26)
}
content
}
.padding(2)
}
}

View File

@ -34,3 +34,29 @@ struct NoContentAvailableView: View
.fillAvailableSpace()
}
}
struct NoContentAvailableViewWithArbitraryImage: View
{
let title: LocalizedStringKey
let image: String
let description: Text? = nil
var body: some View
{
VStack(alignment: .center, spacing: 10)
{
Image(image)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 50, height: 50)
Text(title)
.font(.title)
.multilineTextAlignment(.center)
description
}
.foregroundColor(.gray)
.fillAvailableSpace()
}
}

View File

@ -9,6 +9,7 @@ import SwiftUI
struct DisappearableSheet<Content: View>: View
{
@EnvironmentObject var appState: AppState
@Environment(\.dismiss) var dismiss: DismissAction
@ViewBuilder var sheetContent: Content
@ -20,7 +21,7 @@ struct DisappearableSheet<Content: View>: View
{
DispatchQueue.main.asyncAfter(deadline: .now() + 3)
{
dismiss()
appState.dismissSheet()
}
}
}

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