This commit is contained in:
Alin
2024-04-17 17:36:04 -06:00
parent b9a60f5977
commit 390e23e31d
24 changed files with 1097 additions and 529 deletions
+2 -12
View File
@@ -8,7 +8,6 @@
import SwiftUI
struct AppCommands: Commands {
@AppStorage("settings.general.instant") private var instantSearch: Bool = true
let appState: AppState
let locations: Locations
@@ -42,15 +41,7 @@ struct AppCommands: Commands {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
appState.sortedApps = []
appState.sortedApps = sortedApps
if instantSearch {
Task(priority: .high){
loadAllPaths(allApps: sortedApps, appState: appState, locations: locations) {
appState.reload.toggle()
}
}
} else {
appState.reload.toggle()
}
appState.reload.toggle()
}
}
} label: {
@@ -78,8 +69,7 @@ struct AppCommands: Commands {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
appState.sortedApps = sortedApps
for app in appState.trashedFiles {
let pathFinder = AppPathFinder(appInfo: app, appState: appState, locations: locations)
pathFinder.findPaths()
AppPathFinder(appInfo: app, appState: appState, locations: locations).findPaths()
}
}
}
+1 -1
View File
@@ -62,7 +62,7 @@ class AppInfoFetcher {
let system = !path.path.contains(NSHomeDirectory())
return AppInfo(id: UUID(), path: path, bundleIdentifier: bundleIdentifier, appName: appName, appVersion: appVersion, appIcon: appIcon,
webApp: webApp, wrapped: wrapped, system: system, files: [], fileSize: [:], fileIcon: [:])
webApp: webApp, wrapped: wrapped, system: system, files: [], fileSize: [:], fileSizeLogical: [:], fileIcon: [:])
}
private static func fetchAppIcon(for path: URL, wrapped: Bool) -> NSImage? {
+367 -105
View File
@@ -9,6 +9,7 @@ import Foundation
import AppKit
import SwiftUI
class AppPathFinder {
private var appInfo: AppInfo
private var appState: AppState
@@ -18,7 +19,6 @@ class AppPathFinder {
private var completion: () -> Void = {}
private var collection: [URL] = []
private let collectionAccessQueue = DispatchQueue(label: "com.alienator88.Pearcleaner.appPathFinder.collectionAccess")
@AppStorage("settings.general.instant") var instantSearch: Bool = true
init(appInfo: AppInfo = .empty, appState: AppState, locations: Locations, backgroundRun: Bool = false, reverseAddon: Bool = false, completion: @escaping () -> Void = {}) {
self.appInfo = appInfo
@@ -124,13 +124,24 @@ class AppPathFinder {
collectionAccessQueue.sync {
containsItem = self.collection.contains(itemURL)
}
return itemL.hasPrefix("comapple") && !["comappleconfigurator", "comappledt", "comappleiwork", "comapplesfsymbols", "comappletestflight"].contains(where: itemL.hasPrefix) || containsItem || !isSupportedFileType(at: itemURL.path)
if containsItem || !isSupportedFileType(at: itemURL.path) {
return true
}
for skipCondition in skipConditions {
if itemL.hasPrefix(skipCondition.skipPrefix) {
let isAllowed = skipCondition.allowPrefixes.contains(where: itemL.hasPrefix)
if !isAllowed {
return true // Skip because it starts with a base prefix but is not in the allowed list
}
}
}
return false
}
private func specificCondition(itemL: String, itemURL: URL) -> Bool {
let bundleIdentifierL = self.appInfo.bundleIdentifier.pearFormat()
let bundleComponents = self.appInfo.bundleIdentifier.components(separatedBy: ".").compactMap { $0 != "-" ? $0.lowercased() : nil }
@@ -138,85 +149,20 @@ class AppPathFinder {
let nameL = self.appInfo.appName.pearFormat()
let nameP = self.appInfo.path.lastPathComponent.replacingOccurrences(of: ".app", with: "")
let exclusions = [
"comappledtxcode": ["comrobotsandpencilsxcodesapp", "comoneminutegamesxcodecleaner", "iohyperappxcodecleaner", "xcodesjson"],
"comrobotsandpencilsxcodesapp": ["comappledtxcode", "comoneminutegamesxcodecleaner", "iohyperappxcodecleaner"],
"iohyperappxcodecleaner": ["comrobotsandpencilsxcodesapp", "comoneminutegamesxcodecleaner", "comappledtxcode", "xcodesjson"]
]
for condition in conditions {
if bundleIdentifierL.contains(condition.bundle_id) {
// Exclude and include keywords
let hasIncludeKeyword = condition.include.contains(where: itemL.contains)
let hasExcludeKeyword = condition.exclude.contains(where: itemL.contains)
if let excludedApps = exclusions[bundleIdentifierL], excludedApps.contains(where: { itemL.contains($0) }) {
return false
}
if bundleIdentifierL.contains("comappledtxcode") {
// Include items that are part of the Xcode ecosystem
if itemL.contains("comappledt") || itemL.contains("xcode") || itemL.contains("simulator") {
return true
}
}
if bundleIdentifierL.contains("uszoomxos") {
// Include items for Zoom that are not similar to the app name and/or bundle id
if itemL.contains("zoom") {
return true
}
}
if bundleIdentifierL.contains("combravebrowser") {
// Include items for Brave that are not similar to the app name and/or bundle id
if itemL.contains("brave") {
return true
}
}
if bundleIdentifierL.contains("comgooglechrome") {
// Include items for Chrome that are not similar to the app name and/or bundle id
if (itemL.contains("google") && !itemL.contains("iterm")) || (itemL.contains("chrome") && !itemL.contains("chromefeaturestate")) {
return true
}
}
if bundleIdentifierL.contains("commicrosoftedgemac") {
// Include items for Edge that are not similar to the app name and/or bundle id
let exclusions = ["vscode","rdc","appcenter","office","oneauth"]
if itemL.contains("microsoft") && !exclusions.contains(where: itemL.contains) {
return true
}
}
if bundleIdentifierL.contains("orgmozillafirefox") {
// Include items for Firefox that are not similar to the app name and/or bundle id
if itemL.contains("mozilla") {
return true
}
}
if bundleIdentifierL.contains("comlogioptionsplus") {
// Include items for Zoom that are not similar to the app name and/or bundle id
if itemL.contains("logi") && !itemL.contains("login") && !itemL.contains("logic") {
return true
}
}
if bundleIdentifierL.contains("commicrosoftvscode") {
// Include items for vscode
if itemL.contains("vscode") {
return true
}
}
if bundleIdentifierL.contains("comfacebookarchondeveloperid") {
// Include items for FB Messenger
if itemL.contains("archonloginhelper") {
return true
}
}
if bundleIdentifierL.contains("euexelbanstats") {
// Include items for Stats that are not similar to the app name and/or bundle id
if itemL.contains("video") {
return false
if hasExcludeKeyword {
return false
}
if hasIncludeKeyword {
if !condition.exclude.contains(where: itemL.contains) {
return true
}
}
}
}
@@ -263,37 +209,35 @@ class AppPathFinder {
private func handleOutliers() -> [URL] {
var outliers: [URL] = []
let bundleIdentifier = self.appInfo.bundleIdentifier.pearFormat()
// Handle VSCode folder
if self.appInfo.bundleIdentifier.pearFormat().contains("commicrosoftvscode") {
let appSupportCodePath = "\(home)/Library/Application Support/Code"
if let codeDirectoryURL = URL(string: appSupportCodePath), FileManager.default.fileExists(atPath: codeDirectoryURL.path) {
outliers.append(codeDirectoryURL)
}
// Find conditions that match the current app's bundle identifier
let matchingConditions = conditions.filter { condition in
bundleIdentifier.contains(condition.bundle_id)
}
// Handle Xcode folder
if self.appInfo.bundleIdentifier.pearFormat().contains("comappledtxcode") {
let simulators = "\(home)/Library/Containers/com.apple.iphonesimulator.ShareExtension"
if let simulatorsURL = URL(string: simulators), FileManager.default.fileExists(atPath: simulatorsURL.path) {
outliers.append(simulatorsURL)
for condition in matchingConditions {
if let forceIncludes = condition.includeForce {
for path in forceIncludes {
if let url = URL(string: path), FileManager.default.fileExists(atPath: url.path) {
outliers.append(url)
}
}
}
}
// Add other outlier cases here as needed
return outliers
}
private func finalizeCollection() {
DispatchQueue.global(qos: .userInitiated).async {
let groupContainers = self.getGroupContainers(bundleURL: self.appInfo.path)
// let groupContainers = self.getGroupContainers(bundleURL: self.appInfo.path)
let outliers = self.handleOutliers()
var tempCollection: [URL] = []
self.collectionAccessQueue.sync {
tempCollection = self.collection
}
tempCollection.append(contentsOf: groupContainers)
// tempCollection.append(contentsOf: groupContainers)
tempCollection.append(contentsOf: outliers)
// Sort and standardize URLs to ensure consistent comparisons
@@ -318,10 +262,13 @@ class AppPathFinder {
private func handlePostProcessing(sortedCollection: [URL]) {
// Calculate file details (sizes and icons), update app state, and call completion
var fileSize: [URL: Int64] = [:]
var fileSizeLogical: [URL: Int64] = [:]
var fileIcon: [URL: NSImage?] = [:]
for path in sortedCollection {
fileSize[path] = totalSizeOnDisk(for: path)
let size = totalSizeOnDisk(for: path)
fileSize[path] = size.real
fileSizeLogical[path] = size.logical
fileIcon[path] = getIconForFileOrFolderNS(atPath: path)
}
@@ -332,8 +279,8 @@ class AppPathFinder {
}
// Update appInfo and appState with the new values
self.appInfo.files = updatedCollection
self.appInfo.fileSize = fileSize
self.appInfo.fileSizeLogical = fileSizeLogical
self.appInfo.fileIcon = fileIcon
if !self.backgroundRun {
@@ -341,12 +288,6 @@ class AppPathFinder {
self.appState.selectedItems = Set(updatedCollection)
}
// Only append object to store if instant search. Same for calculating progress.
if self.instantSearch {
self.appState.appInfoStore.append(self.appInfo)
self.appState.instantProgress += 1
}
// Append object to store if running reverse search with empty store
if self.reverseAddon {
self.appState.appInfoStore.append(self.appInfo)
@@ -356,3 +297,324 @@ class AppPathFinder {
}
}
}
// Async Test
//class AppPathFinder {
// private var appInfo: AppInfo
// private var appState: AppState
// private var locations: Locations
// private var backgroundRun: Bool
// private var reverseAddon: Bool
// private var completion: () -> Void = {}
// private var collection: [URL] = []
// private let collectionAccessQueue = DispatchQueue(label: "com.alienator88.Pearcleaner.appPathFinder.collectionAccess")
// private var state = PathFinderState() // Actor instance
//
// init(appInfo: AppInfo = .empty, appState: AppState, locations: Locations, backgroundRun: Bool = false, reverseAddon: Bool = false, completion: @escaping () -> Void = {}) {
// self.appInfo = appInfo
// self.appState = appState
// self.locations = locations
// self.backgroundRun = backgroundRun
// self.reverseAddon = reverseAddon
// self.completion = completion
// }
//
// func findPaths() async {
// await initialURLProcessing()
// await collectDirectories()
// await collectFiles()
// await finalizeCollection()
// }
//
// private func initialURLProcessing() async {
// if let url = URL(string: self.appInfo.path.absoluteString), !url.path.contains(".Trash") {
// let modifiedUrl = url.path.contains("Wrapper") ? url.deletingLastPathComponent().deletingLastPathComponent() : url
// collectionAccessQueue.sync {
// self.collection.append(modifiedUrl)
// }
// }
// }
//
// private func collectDirectories() async {
// for location in self.locations.apps.paths {
// await processDirectoryLocation(location)
// }
// }
//
// private func processDirectoryLocation(_ location: String) async {
// do {
// let contents = try FileManager.default.contentsOfDirectory(atPath: location)
// for item in contents {
// let itemURL = URL(fileURLWithPath: location).appendingPathComponent(item)
// let itemL = item.replacingOccurrences(of: ".", with: "").replacingOccurrences(of: " ", with: "").lowercased()
//
// var isDirectory: ObjCBool = false
// if FileManager.default.fileExists(atPath: itemURL.path, isDirectory: &isDirectory), isDirectory.boolValue {
// // Perform the check to skip the item if needed
// if shouldSkipItem(itemL, at: itemURL) {
// continue
// }
//
// collectionAccessQueue.sync {
// let alreadyIncluded = self.collection.contains { existingURL in
// itemURL.path.hasPrefix(existingURL.path)
// }
//
// if !alreadyIncluded && specificCondition(itemL: itemL, itemURL: itemURL) {
// self.collection.append(itemURL)
// }
// }
// }
// }
// } catch {
// print("Error processing directory location: \(location), error: \(error)")
// }
// }
//
// private func collectFiles() async {
// for location in self.locations.apps.paths {
// await processFileLocation(location)
// }
// }
//
// private func processFileLocation(_ location: String) async {
// do {
// let contents = try FileManager.default.contentsOfDirectory(atPath: location)
// for item in contents {
// let itemURL = URL(fileURLWithPath: location).appendingPathComponent(item)
// let itemL = item.replacingOccurrences(of: ".", with: "").replacingOccurrences(of: " ", with: "").lowercased()
//
// if FileManager.default.fileExists(atPath: itemURL.path),
// !shouldSkipItem(itemL, at: itemURL),
// specificCondition(itemL: itemL, itemURL: itemURL) {
// collectionAccessQueue.sync {
// self.collection.append(itemURL)
// }
// }
// }
// } catch {
// print("Error processing file location: \(location), error: \(error)")
// }
// }
//
//
//
// private func shouldSkipItem(_ itemL: String, at itemURL: URL) -> Bool {
// var containsItem = false
// collectionAccessQueue.sync {
// containsItem = self.collection.contains(itemURL)
// }
// if containsItem || !isSupportedFileType(at: itemURL.path) {
// return true
// }
//
// for skipCondition in skipConditions {
// if itemL.hasPrefix(skipCondition.skipPrefix) {
// let isAllowed = skipCondition.allowPrefixes.contains(where: itemL.hasPrefix)
// if !isAllowed {
// return true // Skip because it starts with a base prefix but is not in the allowed list
// }
// }
// }
//
// return false
// }
//
//
//
// private func specificCondition(itemL: String, itemURL: URL) -> Bool {
// let bundleIdentifierL = self.appInfo.bundleIdentifier.pearFormat()
// let bundleComponents = self.appInfo.bundleIdentifier.components(separatedBy: ".").compactMap { $0 != "-" ? $0.lowercased() : nil }
// let bundle = bundleComponents.suffix(2).joined()
// let nameL = self.appInfo.appName.pearFormat()
// let nameP = self.appInfo.path.lastPathComponent.replacingOccurrences(of: ".app", with: "")
//
// for condition in conditions {
// if bundleIdentifierL.contains(condition.bundle_id) {
// // Exclude and include keywords
// let hasIncludeKeyword = condition.include.contains(where: itemL.contains)
// let hasExcludeKeyword = condition.exclude.contains(where: itemL.contains)
//
// if hasExcludeKeyword {
// return false
// }
// if hasIncludeKeyword {
// if !condition.exclude.contains(where: itemL.contains) {
// return true
// }
// }
// }
// }
//
//
// if self.appInfo.webApp {
// return itemL.contains(bundleIdentifierL)
// }
//
// return itemL.contains(bundleIdentifierL) || itemL.contains(bundle) || (nameL.count > 3 && itemL.contains(nameL)) || (nameP.count > 3 && itemL.contains(nameP))
//
// }
//
//
// func getGroupContainers(bundleURL: URL) async -> [URL] {
// await withCheckedContinuation { continuation in
// var groupContainers: [URL] = []
// // Assume creating a SecStaticCode is synchronous and quick.
// var staticCode: SecStaticCode?
// let status = SecStaticCodeCreateWithPath(bundleURL as CFURL, [], &staticCode)
//
// if status == errSecSuccess, let staticCode = staticCode {
// var signingInformation: CFDictionary?
//
// // This may involve a blocking call to fetch signing information.
// let status = SecCodeCopySigningInformation(staticCode, SecCSFlags(), &signingInformation)
//
// if status == errSecSuccess, let infoDict = signingInformation as? [String: Any],
// let entitlementsDict = infoDict["entitlements-dict"] as? [String: Any],
// let appGroups = entitlementsDict["com.apple.security.application-groups"] as? [String] {
//
// let fileManager = FileManager.default
// let home = NSHomeDirectory() // Assuming `home` is derived earlier or statically known.
//
// for groupID in appGroups {
// let groupURL = URL(fileURLWithPath: "\(home)/Library/Group Containers/\(groupID)")
// if fileManager.fileExists(atPath: groupURL.path) {
// groupContainers.append(groupURL)
// }
// }
// }
// }
//
// // Continue once all operations are complete.
// continuation.resume(returning: groupContainers)
// }
// }
//
//
// private func handleOutliers() -> [URL] {
// var outliers: [URL] = []
// let bundleIdentifier = self.appInfo.bundleIdentifier.pearFormat()
//
// // Find conditions that match the current app's bundle identifier
// let matchingConditions = conditions.filter { condition in
// bundleIdentifier.contains(condition.bundle_id)
// }
//
// for condition in matchingConditions {
// if let forceIncludes = condition.includeForce {
// for path in forceIncludes {
// if let url = URL(string: path), FileManager.default.fileExists(atPath: url.path) {
// outliers.append(url)
// }
// }
// }
// }
//
// return outliers
// }
//
// private func finalizeCollection() async {
// let groupContainers = await getGroupContainers(bundleURL: self.appInfo.path)
// let outliers = handleOutliers()
// var tempCollection: [URL] = []
//
// collectionAccessQueue.sync {
// tempCollection = self.collection
// }
//
// tempCollection.append(contentsOf: groupContainers)
// tempCollection.append(contentsOf: outliers)
//
// // Sort and standardize URLs to ensure consistent comparisons
// let sortedCollection = tempCollection.map { $0.standardizedFileURL }.sorted(by: { $0.path < $1.path })
// var filteredCollection: [URL] = []
// var previousUrl: URL?
//
// for url in sortedCollection {
// if let previous = previousUrl, url.path.hasPrefix(previous.path + "/") {
// // Current URL is a subdirectory of the previous one, so skip it
// continue
// }
// // This URL is not a subdirectory of the previous one, so keep it and set it as the previous URL
// filteredCollection.append(url)
// previousUrl = url
// }
//
// await handlePostProcessing(sortedCollection: filteredCollection)
// }
//
// private func handlePostProcessing(sortedCollection: [URL]) async {
// for path in sortedCollection {
// let size = totalSizeOnDisk(for: path)
// if let icon = getIconForFileOrFolderNS(atPath: path) { // Retrieve NSImage
// let iconData = serializeImage(icon) // Convert NSImage to Data
// await state.setFileDetails(for: path, size: size, icon: iconData) // Pass Data to actor
// } else {
// await state.setFileDetails(for: path, size: size, icon: nil)
// }
// }
// await updateAppState(with: sortedCollection)
// }
//
// private func updateAppState(with sortedCollection: [URL]) async {
// // Retrieve state from the actor
// let fileSize = await self.state.getFileSize()
// let fileIconData = await self.state.getFileIconData()
// let fileIcons = fileIconData.mapValues { deserializeImage($0) }
//
// // Assume updating of appInfo needs to happen on the main thread
// self.appInfo.fileSize = fileSize
// self.appInfo.fileIcon = fileIcons
//
// // Execute UI related updates on the main thread using MainActor
// await MainActor.run {
// if !self.backgroundRun {
// self.appState.appInfo = self.appInfo
// self.appState.selectedItems = Set(sortedCollection)
// }
//
//
// }
//
// self.completion() // Call the completion handler
// }
//}
//
//actor PathFinderState {
// var fileSize: [URL: Int64] = [:]
// var fileIconData: [URL: Data?] = [:]
//
// func setFileDetails(for path: URL, size: Int64, icon: Data?) {
// fileSize[path] = size
// fileIconData[path] = icon
// }
//
// func getFileSize() -> [URL: Int64] {
// return fileSize
// }
//
// func getFileIconData() -> [URL: Data?] {
// return fileIconData
// }
//}
//
//
//func serializeImage(_ image: NSImage?) -> Data? {
// guard let image = image else { return nil }
// guard let tiffData = image.tiffRepresentation else { return nil }
// let bitmapImage = NSBitmapImageRep(data: tiffData)
// return bitmapImage?.representation(using: .png, properties: [:])
//}
//
//func deserializeImage(_ data: Data?) -> NSImage? {
// guard let data = data else { return nil }
// return NSImage(data: data)
//}
+14 -7
View File
@@ -29,9 +29,6 @@ class AppState: ObservableObject
@Published var progressBar: (String, Double) = ("Ready", 0.0)
@Published var reload: Bool = false
@Published var showProgress: Bool = false
@Published var popCount: Int = 0
@Published var instantProgress: Double = 0.0
@Published var instantTotal: Double = 0.0
@Published var finderExtensionEnabled: Bool = false
@@ -48,12 +45,14 @@ class AppState: ObservableObject
system: false,
files: [],
fileSize: [:],
fileSizeLogical: [:],
fileIcon: [:]
)
self.zombieFile = ZombieFile(
id: UUID(),
fileSize: [:],
fileSizeLogical: [:],
fileIcon: [:]
)
updateExtensionStatus()
@@ -91,14 +90,19 @@ struct AppInfo: Identifiable, Equatable, Hashable {
let system: Bool
var files: [URL]
var fileSize: [URL:Int64]
var fileSizeLogical: [URL:Int64]
var fileIcon: [URL:NSImage?]
var totalSize: Int64
{
return fileSize.values.reduce(0, +)
}
var totalSizeLogical: Int64
{
return fileSizeLogical.values.reduce(0, +)
}
static let empty = AppInfo(id: UUID(), path: URL(fileURLWithPath: ""), bundleIdentifier: "", appName: "", appVersion: "", appIcon: nil, webApp: false, wrapped: false, system: false, files: [], fileSize: [:], fileIcon: [:])
static let empty = AppInfo(id: UUID(), path: URL(fileURLWithPath: ""), bundleIdentifier: "", appName: "", appVersion: "", appIcon: nil, webApp: false, wrapped: false, system: false, files: [], fileSize: [:], fileSizeLogical: [:], fileIcon: [:])
}
@@ -107,14 +111,19 @@ struct AppInfo: Identifiable, Equatable, Hashable {
struct ZombieFile: Identifiable, Equatable, Hashable {
let id: UUID
var fileSize: [URL:Int64]
var fileSizeLogical: [URL:Int64]
var fileIcon: [URL:NSImage?]
var totalSize: Int64
{
return fileSize.values.reduce(0, +)
}
var totalSizeLogical: Int64
{
return fileSizeLogical.values.reduce(0, +)
}
static let empty = ZombieFile(id: UUID(), fileSize: [:], fileIcon: [:])
static let empty = ZombieFile(id: UUID(), fileSize: [:], fileSizeLogical: [:], fileIcon: [:])
}
@@ -201,5 +210,3 @@ enum DisplayMode: Int, CaseIterable {
}
}
}
+149
View File
@@ -0,0 +1,149 @@
//
// Conditions.swift
// Pearcleaner
//
// Created by Alin Lupascu on 4/15/24.
//
import Foundation
struct Condition: Decodable {
var bundle_id: String
var include: [String]
var exclude: [String]
var includeForce: [String]?
}
struct SkipCondition {
var skipPrefix: String
var allowPrefixes: [String]
}
// Conditions for some apps that need to include/exclude certain files/folders when names are more complicated
var conditions: [Condition] = [
Condition(
bundle_id: "comappledtxcode",
include: ["comappledt", "xcode", "simulator"],
exclude: ["comrobotsandpencilsxcodesapp", "comoneminutegamesxcodecleaner", "iohyperappxcodecleaner", "xcodesjson"],
includeForce: ["\(home)/Library/Containers/com.apple.iphonesimulator.ShareExtension"]
),
Condition(
bundle_id: "comrobotsandpencilsxcodesapp",
include: [],
exclude: ["comappledtxcode", "comoneminutegamesxcodecleaner", "iohyperappxcodecleaner"]
),
Condition(
bundle_id: "iohyperappxcodecleaner",
include: [],
exclude: ["comrobotsandpencilsxcodesapp", "comoneminutegamesxcodecleaner", "comappledtxcode", "xcodesjson"]
),
Condition(
bundle_id: "uszoomxos",
include: ["zoom"],
exclude: []
),
Condition(
bundle_id: "combravebrowser",
include: ["brave"],
exclude: []
),
Condition(
bundle_id: "comoktamobile",
include: ["okta"],
exclude: []
),
Condition(
bundle_id: "comgooglechrome",
include: ["google", "chrome"],
exclude: ["iterm", "chromefeaturestate"]
),
Condition(
bundle_id: "commicrosoftedgemac",
include: ["microsoft"],
exclude: ["vscode", "rdc", "appcenter", "office", "oneauth"]
),
Condition(
bundle_id: "orgmozillafirefox",
include: ["mozilla"],
exclude: []
),
Condition(
bundle_id: "comlogioptionsplus",
include: ["logi"],
exclude: ["login", "logic"],
includeForce: []
),
Condition(
bundle_id: "commicrosoftvscode",
include: ["vscode"],
exclude: [],
includeForce: ["\(home)/Library/Application Support/Code"]
),
Condition(
bundle_id: "comfacebookarchondeveloperid",
include: ["archonloginhelper"],
exclude: []
),
Condition(
bundle_id: "euexelbanstats",
include: [],
exclude: ["video"]
),
Condition(
bundle_id: "jetbrains",
include: ["jetbrains", "jcef"],
exclude: []
),
]
// Skip com.apple files/folders since most are system originated, allow some for apps
let skipConditions: [SkipCondition] = [
SkipCondition(
skipPrefix: "comapple",
allowPrefixes: ["comappleconfigurator", "comappledt", "comappleiwork", "comapplesfsymbols", "comappletestflight"]
)
]
// Skip files/folders during reverse file search
let skipReverse = ["apple", "temporary", "btserver", "proapps", "scripteditor", "ilife", "livefsd", "siritoday", "addressbook", "animoji", "appstore", "askpermission", "callhistory", "clouddocs", "diskimages", "dock", "facetime", "fileprovider", "instruments", "knowledge", "mobilesync", "syncservices", "homeenergyd", "icloud", "icdd", "networkserviceproxy", "familycircle", "geoservices", "installation", "passkit", "sharedimagecache", "desktop", "mbuseragent", "swiftpm", "baseband", "coresimulator", "photoslegacyupgrade", "photosupgrade", "siritts", "ipod", "globalpreferences", "apmanalytics", "apmexperiment", "avatarcache", "byhost", "contextstoreagent", "mobilemeaccounts", "intentbuilderc", "loginwindow", "momc", "replayd", "sharedfilelistd", "clang", "audiocomponent", "csexattrcryptoservice", "livetranscriptionagent", "sandboxhelper", "statuskitagent", "betaenrollmentd", "contentlinkingd", "diagnosticextensionsd", "gamed", "heard", "homed", "itunescloudd", "lldb", "mds", "mediaanalysisd", "metrickitd", "mobiletimerd", "proactived", "ptpcamerad", "studentd", "talagent", "watchlistd", "apptranslocation", "xcrun", "ds_store", "caches", "crashreporter", "trash", "pearcleaner", "amsdatamigratortool", "arfilecache", "assistant", "chromium", "cloudkit", "webkit", "databases", "diagnostic", "cache", "gamekit", "homebrew", "logi", "microsoft", "mozilla", "sync", "google", "sentinel", "hexnode", "sentry", "tvappservices"]
// Function to load additional conditions from a GitHub JSON file
func loadConditionsFromGitHub() {
let url = URL(string: "https://api.github.com/repos/alienator88/Pearcleaner/contents/conditions.json")!
var request = URLRequest(url: url)
request.setValue("application/vnd.github.VERSION.raw", forHTTPHeaderField: "Accept")
request.setValue("2022-11-28", forHTTPHeaderField: "X-GitHub-Api-Version")
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
do {
// Assuming the JSON structure directly maps to an array of Condition
let jsonObject = try JSONSerialization.jsonObject(with: data, options: [])
if let conditionArray = jsonObject as? [[String: Any]] {
let jsonData = try JSONSerialization.data(withJSONObject: conditionArray, options: [])
let additionalConditions = try JSONDecoder().decode([Condition].self, from: jsonData)
if !additionalConditions.isEmpty {
DispatchQueue.main.async {
conditions += additionalConditions
}
}
} else {
printOS("The data format is incorrect or empty for GitHub conditions processing.")
}
} catch {
printOS("Failed to decode conditions JSON: \(error.localizedDescription)")
}
} else {
printOS("Failed to fetch conditions data: \(error?.localizedDescription ?? "Unknown error")")
}
}.resume()
}
-32
View File
@@ -19,8 +19,6 @@ class Locations: ObservableObject {
var apps: Category
var reverse: Category
// var widgets: Category
// var plugins: Category
init() {
let (cacheDir, tempDir) = darwinCT()
@@ -103,35 +101,5 @@ class Locations: ObservableObject {
cacheDir,
tempDir
])
// self.widgets = Category(name: "Widgets", paths: [
// // User
// "~/Library/Widgets",
// // System
// "/Library/Widgets"
// ])
//
// self.plugins = Category(name: "Plugins", paths: [
// // User
// "~/Library/Contextual Menu Items",
// "~/Library/InputManagers",
// "~/Library/Internet Plug-Ins",
// "~/Library/Mail/Bundles",
// "~/Library/PreferencePanes",
// "~/Library/QuickLook",
// "~/Library/QuickTime",
// "~/Library/Screen Savers",
// "~/Library/Spotlight",
// // System
// "/Library/Contextual Menu Items",
// "/Library/InputManagers",
// "/Library/Internet Plug-Ins",
// "/Library/Mail/Bundles",
// "/Library/PreferencePanes",
// "/Library/QuickLook",
// "/Library/QuickTime",
// "/Library/Screen Savers",
// "/Library/Spotlight",
// ])
}
}
+8 -26
View File
@@ -11,7 +11,6 @@ import SwiftUI
// Get all apps from /Applications and ~/Applications
func getSortedApps(paths: [String], appState: AppState) -> [AppInfo] {
@AppStorage("settings.general.instant") var instantSearch: Bool = true
let fileManager = FileManager.default
var apps: [URL] = []
@@ -42,11 +41,6 @@ func getSortedApps(paths: [String], appState: AppState) -> [AppInfo] {
.compactMap { AppInfoFetcher.getAppInfo(atPath: $0) }
.sorted { $0.appName.replacingOccurrences(of: ".", with: "").lowercased() < $1.appName.replacingOccurrences(of: ".", with: "").lowercased() }
if instantSearch {
updateOnMain {
appState.instantTotal = Double(sortedApps.count)
}
}
return sortedApps
}
@@ -116,15 +110,14 @@ func listAppSupportDirectories() -> [String] {
// Load app paths on launch
func loadAllPaths(allApps: [AppInfo], appState: AppState, locations: Locations, reverseAddon: Bool = false, completion: @escaping () -> Void = {}) {
func reversePreloader(allApps: [AppInfo], appState: AppState, locations: Locations, reverseAddon: Bool = false, completion: @escaping () -> Void = {}) {
let dispatchGroup = DispatchGroup()
appState.appInfoStore.removeAll()
for app in allApps {
dispatchGroup.enter()
DispatchQueue.global(qos: .background).async {
let pathFinder = AppPathFinder(appInfo: app, appState: appState, locations: locations, backgroundRun: true, reverseAddon: reverseAddon)
pathFinder.findPaths()
AppPathFinder(appInfo: app, appState: appState, locations: locations, backgroundRun: true, reverseAddon: reverseAddon).findPaths()
dispatchGroup.leave()
}
}
@@ -134,28 +127,17 @@ func loadAllPaths(allApps: [AppInfo], appState: AppState, locations: Locations,
func checkAllAppsProcessed(retryCount: Int = 0, maxRetry: Int = 120) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
if appState.appInfoStore.count == allApps.count {
if reverseAddon {
let reverse = ReversePathsSearcher(appState: appState, locations: locations)
reverse.reversePathsSearch() {
updateOnMain {
appState.showProgress = false
}
ReversePathsSearcher(appState: appState, locations: locations).reversePathsSearch() {
updateOnMain {
appState.showProgress = false
}
}
// Reset progress values to 0
updateOnMain {
appState.instantProgress = 0
appState.instantTotal = 0
appState.showProgress = false
}
completion()
} else if retryCount < maxRetry {
checkAllAppsProcessed(retryCount: retryCount + 1, maxRetry: maxRetry)
} else {
// Reset progress values to 0
updateOnMain {
appState.instantProgress = 0
appState.instantTotal = 0
appState.showProgress = false
}
printOS("loadAllPaths - Retry limit reached. Not all paths were loaded.")
@@ -196,13 +178,13 @@ func showAppInFiles(appInfo: AppInfo, appState: AppState, locations: Locations,
appState.showProgress = true
// Initialize the path finder and execute its search.
let pathFinder = AppPathFinder(appInfo: appInfo, appState: appState, locations: locations) {
AppPathFinder(appInfo: appInfo, appState: appState, locations: locations) {
updateOnMain {
// Update the progress indicator on the main thread once the search completes.
appState.showProgress = false
}
}
pathFinder.findPaths()
}.findPaths()
appState.appInfo = appInfo
// Animate the view change and popover display.
+11 -6
View File
@@ -14,16 +14,17 @@ class ReversePathsSearcher {
private let fileManager = FileManager.default
private var collection: [URL] = []
private var fileSize: [URL: Int64] = [:]
private var fileSizeLogical: [URL: Int64] = [:]
private var fileIcon: [URL: NSImage?] = [:]
private var skipped: Set<String>
private let dispatchGroup = DispatchGroup()
init(appState: AppState, locations: Locations) {
self.appState = appState
self.locations = locations
self.skipped = ["apple", "temporary", "btserver", "proapps", "scripteditor", "ilife", "livefsd", "siritoday", "addressbook", "animoji", "appstore", "askpermission", "callhistory", "clouddocs", "diskimages", "dock", "facetime", "fileprovider", "instruments", "knowledge", "mobilesync", "syncservices", "homeenergyd", "icloud", "icdd", "networkserviceproxy", "familycircle", "geoservices", "installation", "passkit", "sharedimagecache", "desktop", "mbuseragent", "swiftpm", "baseband", "coresimulator", "photoslegacyupgrade", "photosupgrade", "siritts", "ipod", "globalpreferences", "apmanalytics", "apmexperiment", "avatarcache", "byhost", "contextstoreagent", "mobilemeaccounts", "intentbuilderc", "loginwindow", "momc", "replayd", "sharedfilelistd", "clang", "audiocomponent", "csexattrcryptoservice", "livetranscriptionagent", "sandboxhelper", "statuskitagent", "betaenrollmentd", "contentlinkingd", "diagnosticextensionsd", "gamed", "heard", "homed", "itunescloudd", "lldb", "mds", "mediaanalysisd", "metrickitd", "mobiletimerd", "proactived", "ptpcamerad", "studentd", "talagent", "watchlistd", "apptranslocation", "xcrun", "ds_store", "caches", "crashreporter", "trash", "pearcleaner", "amsdatamigratortool", "arfilecache", "assistant", "chromium", "cloudkit", "webkit", "databases", "diagnostic", "cache", "gamekit", "homebrew", "logi", "microsoft", "mozilla", "sync", "google", "sentinel", "hexnode", "sentry", "tvappservices"]
}
func reversePathsSearch(completion: @escaping () -> Void = {}) {
Task(priority: .high) {
self.processLocations()
@@ -34,17 +35,19 @@ class ReversePathsSearcher {
}
private func processLocations() {
let allPaths = appState.appInfoStore.flatMap { $0.files.map { $0.path.pearFormat() } }
let allPaths = appState.appInfoStore.flatMap { $0.fileSize.keys.map { $0.path.pearFormat() } }
let allNames = appState.appInfoStore.map { $0.appName.pearFormat() }
for location in locations.reverse.paths where fileManager.fileExists(atPath: location) {
dispatchGroup.enter()
processLocation(location, allPaths: allPaths, allNames: allNames)
dispatchGroup.leave()
}
}
private func processLocation(_ location: String, allPaths: [String], allNames: [String]) {
do {
let contents = try fileManager.contentsOfDirectory(atPath: location)
contents.forEach { itemName in
@@ -61,8 +64,7 @@ class ReversePathsSearcher {
let itemPath = itemURL.path.pearFormat()
let itemLastPathComponent = itemURL.lastPathComponent.pearFormat()
guard !skipped.contains(where: { formattedItemName.contains($0) }),
// !allPaths.contains(itemPath),
guard !skipReverse.contains(where: { formattedItemName.contains($0) }),
!allPaths.contains(where: { $0 == itemPath || $0.hasSuffix("/\(itemLastPathComponent)") }),
!allNames.contains(formattedItemName),
isSupportedFileType(at: itemURL.path) else {
@@ -74,7 +76,9 @@ class ReversePathsSearcher {
private func calculateFileDetails() {
collection.forEach { path in
fileSize[path] = totalSizeOnDisk(for: path)
let size = totalSizeOnDisk(for: path)
fileSize[path] = size.real
fileSizeLogical[path] = size.logical
fileIcon[path] = getIconForFileOrFolderNS(atPath: path)
}
}
@@ -83,6 +87,7 @@ class ReversePathsSearcher {
dispatchGroup.notify(queue: .main) {
var updatedZombieFile = ZombieFile.empty
updatedZombieFile.fileSize = self.fileSize
updatedZombieFile.fileSizeLogical = self.fileSizeLogical
updatedZombieFile.fileIcon = self.fileIcon
self.appState.zombieFile = updatedZombieFile
self.appState.showProgress = false
+3 -48
View File
@@ -441,56 +441,11 @@ struct SimpleSearchStyle: TextFieldStyle {
}
}
.disabled(appState.instantProgress != 0 && appState.instantProgress != appState.instantTotal)
.padding(6)
.overlay(
Group {
GeometryReader { geometry in
ZStack(alignment: .leading) {
if appState.instantProgress != 0 && appState.instantProgress != appState.instantTotal {
let totalWidth = geometry.size.width
let progressFraction = CGFloat(appState.instantProgress / appState.instantTotal)
let progressWidth = totalWidth * progressFraction
RoundedRectangle(cornerRadius: 6)
.fill(Color.accentColor)
.padding(4)
.frame(width: progressWidth)
.animation(.linear, value: progressWidth)
.allowsHitTesting(false)
}
RoundedRectangle(cornerRadius: 6)
.strokeBorder(Color("mode").opacity(0.2), lineWidth: 1)
.allowsHitTesting(false)
}
}
// ZStack {
// RoundedRectangle(cornerRadius: 6)
// .strokeBorder(Color("mode").opacity(0.2), lineWidth: 1)
// .allowsHitTesting(false)
//
// if appState.instantTotal > 0 {
// let progressWidth = CGFloat(appState.instantProgress / appState.instantTotal)
// * NSScreen.main.bounds.width // Assuming full width for simplicity; adjust as needed
// RoundedRectangle(cornerRadius: 6)
// .fill(Color.accentColor.opacity(0.5)) // Adjust color and opacity as needed
// .frame(width: progressWidth)
// .animation(.linear, value: progressWidth)
// .allowsHitTesting(false)
// }
//// if appState.instantProgress != 0 && appState.instantProgress != appState.instantTotal {
//// ProgressView("", value: appState.instantProgress, total: appState.instantTotal)
//// .progressViewStyle(.linear)
//// .padding()
//// }
// }
}
RoundedRectangle(cornerRadius: 6)
.strokeBorder(Color("mode").opacity(0.2), lineWidth: 1)
.allowsHitTesting(false)
)
.onHover { hovering in
withAnimation(Animation.easeInOut(duration: 0.15)) {
+46
View File
@@ -28,10 +28,12 @@ class ThemeSettings: ObservableObject {
if let components = UserDefaults.standard.object(forKey: colorKey) as? [CGFloat], components.count >= 4 {
themeColor = Color(.sRGB, red: components[0], green: components[1], blue: components[2], opacity: components[3])
} else {
themeColor = Color.clear
}
}
func saveThemeColor() {
let nsColor = NSColor(themeColor)
if let components = nsColor.cgColor.components {
@@ -43,17 +45,61 @@ class ThemeSettings: ObservableObject {
themeColor = dark ? Color(.sRGB, red: 0.149, green: 0.149, blue: 0.149, opacity: 1) : Color(.sRGB, red: 1.0, green: 1.0, blue: 1.0, opacity: 1)
saveThemeColor()
}
func setPreset(preset: String, colorScheme: DisplayMode) {
themeColor = getColorForPreset(preset: preset, colorScheme: colorScheme)
saveThemeColor()
}
func getColorForPreset(preset: String, colorScheme: DisplayMode) -> Color {
let slate = colorScheme.colorScheme == .light ? Color(.sRGB, red: 0.499549, green: 0.545169, blue: 0.682028, opacity: 1) : Color(.sRGB, red: 0.188143, green: 0.208556, blue: 0.262679, opacity: 1)
let solarized = colorScheme.colorScheme == .light ? Color(.sRGB, red: 0.554372, green: 0.6557, blue: 0.734336, opacity: 1) : Color(.sRGB, red: 0.117257, green: 0.22506, blue: 0.249171, opacity: 1)
let dracula = colorScheme.colorScheme == .light ? Color(.sRGB, red: 0.567094, green: 0.562125, blue: 0.81285, opacity: 1) : Color(.sRGB, red: 0.268614, green: 0.264737, blue: 0.383503, opacity: 1)
switch preset {
case "slate":
return slate
case "solarized":
return solarized
case "dracula":
return dracula
default:
return themeColor
}
}
}
extension Color {
/// Darkens the color by a percentage.
func darker(by percentage: CGFloat = 30.0) -> Color {
var hsb = (hue: CGFloat(0), saturation: CGFloat(0), brightness: CGFloat(0), alpha: CGFloat(0))
NSColor(self).getHue(&hsb.hue, saturation: &hsb.saturation, brightness: &hsb.brightness, alpha: &hsb.alpha)
return Color(hue: hsb.hue, saturation: hsb.saturation, brightness: max(hsb.brightness - percentage/100, 0), opacity: hsb.alpha)
}
/// Lightens the color by a percentage.
func lighter(by percentage: CGFloat = 30.0) -> Color {
var hsb = (hue: CGFloat(0), saturation: CGFloat(0), brightness: CGFloat(0), alpha: CGFloat(0))
NSColor(self).getHue(&hsb.hue, saturation: &hsb.saturation, brightness: &hsb.brightness, alpha: &hsb.alpha)
return Color(hue: hsb.hue, saturation: hsb.saturation, brightness: min(hsb.brightness + percentage / 100, 1.0), opacity: hsb.alpha)
}
}
extension UserDefaults {
func color(forKey key: String) -> Color? {
guard let components = array(forKey: key) as? [CGFloat], components.count == 4 else {
return nil
}
return Color(.sRGB, red: components[0], green: components[1], blue: components[2], opacity: components[3])
}
func setColor(_ color: Color, forKey key: String) {
let nsColor = NSColor(color)
if let components = nsColor.cgColor.components, components.count == 4 {
set(components, forKey: key)
} else {
removeObject(forKey: key)
}
}
}
+49 -65
View File
@@ -174,20 +174,13 @@ func isCurrentUserAdmin(completion: @escaping (Bool) -> Void) {
}
// Check if appearance is dark mode
func isDarkModeEnabled() -> Bool {
let process = Process()
process.executableURL = URL(fileURLWithPath: "/bin/zsh")
process.arguments = ["-c", "defaults read -g AppleInterfaceStyle"]
let pipe = Pipe()
process.standardOutput = pipe
process.standardError = pipe
try? process.run()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
if !data.isEmpty{
return true
} else {
return false
}
func isDarkMode() -> Bool {
return NSApp.effectiveAppearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua
}
// Set app color mode
func setAppearance(mode: NSAppearance.Name) {
NSApp.appearance = NSAppearance(named: mode)
}
// Check if Pearcleaner has any windows open
@@ -216,6 +209,13 @@ func findAndShowWindows(named titles: [String]) {
}
}
// Copy to clipboard
func copyToClipboard(text: String) {
let pasteboard = NSPasteboard.general
pasteboard.clearContents()
pasteboard.setString(text, forType: .string)
}
// Check if appearance is dark mode
//func getCasks() -> [String] {
@@ -458,86 +458,73 @@ func printOS(_ items: Any..., separator: String = " ", terminator: String = "\n"
}
//func totalSizeOnDisk2(for paths: [URL]) -> Int64 {
// var totalSize = 0
//
// let process = Process()
// process.launchPath = "/usr/bin/du"
// process.arguments = ["-sk"] + paths.map { (url: URL) -> String in
// return url.path
// }
// let pipe = Pipe()
// process.standardOutput = pipe
// process.standardError = pipe//FileHandle.nullDevice
//
// try? process.run()
// process.waitUntilExit()
//
// let data = pipe.fileHandleForReading.readDataToEndOfFile()
// if let output = String(data: data, encoding: .utf8) {
// let lines = output.components(separatedBy: .newlines)
// for line in lines {
// let components = line.components(separatedBy: "\t")
// if let sizeString = components.first, let size = Int(sizeString) {
// totalSize += size
// }
// }
// }
//
// return Int64(totalSize) * 1024 // Convert the size from kilobytes to bytes
//}
// Get total size of folders and files using DU cli command
func totalSizeOnDisk(for paths: [URL]) -> Int64 {
// Get size of files
func totalSizeOnDisk(for paths: [URL]) -> (real: Int64, logical: Int64) {
let fileManager = FileManager.default
var totalSize: Int64 = 0
var totalAllocatedSize: Int64 = 0
var totalFileSize: Int64 = 0
for url in paths {
var isDirectory: ObjCBool = false
if fileManager.fileExists(atPath: url.path, isDirectory: &isDirectory) {
let keys: [URLResourceKey] = [.totalFileAllocatedSizeKey, .fileSizeKey]
if isDirectory.boolValue {
// It's a directory, recurse into it
if let enumerator = fileManager.enumerator(at: url, includingPropertiesForKeys: [.totalFileAllocatedSizeKey], errorHandler: nil) {
if let enumerator = fileManager.enumerator(at: url, includingPropertiesForKeys: keys, errorHandler: nil) {
for case let fileURL as URL in enumerator {
do {
let fileAttributes = try fileURL.resourceValues(forKeys: [.totalFileAllocatedSizeKey])
if let fileSize = fileAttributes.totalFileAllocatedSize {
totalSize += Int64(fileSize)
let fileAttributes = try fileURL.resourceValues(forKeys: Set(keys))
if let allocatedSize = fileAttributes.totalFileAllocatedSize {
totalAllocatedSize += Int64(allocatedSize)
}
if let fileSize = fileAttributes.fileSize {
totalFileSize += Int64(fileSize)
}
} catch {
printOS("Error getting file size for \(fileURL): \(error)")
print("Error getting file attributes for \(fileURL): \(error)")
}
}
}
} else {
// It's a file
do {
let fileAttributes = try url.resourceValues(forKeys: [.totalFileAllocatedSizeKey])
if let fileSize = fileAttributes.totalFileAllocatedSize {
totalSize += Int64(fileSize)
let fileAttributes = try url.resourceValues(forKeys: Set(keys))
if let allocatedSize = fileAttributes.totalFileAllocatedSize {
totalAllocatedSize += Int64(allocatedSize)
}
if let fileSize = fileAttributes.fileSize {
totalFileSize += Int64(fileSize)
}
} catch {
printOS("Error getting file size for \(url): \(error)")
print("Error getting file attributes for \(url): \(error)")
}
}
}
}
return totalSize
return (real: totalAllocatedSize, logical: totalFileSize)
}
func totalSizeOnDisk(for path: URL) -> Int64 {
func totalSizeOnDisk(for path: URL) -> (real: Int64, logical: Int64) {
return totalSizeOnDisk(for: [path])
}
// ByteFormatter
func formatByte(size: Int64) -> String {
func formatByte(size: Int64) -> (human: String, byte: String) {
let byteCountFormatter = ByteCountFormatter()
byteCountFormatter.countStyle = .file
byteCountFormatter.allowedUnits = [.useAll]
return byteCountFormatter.string(fromByteCount: size)
let human = byteCountFormatter.string(fromByteCount: size)
let numberformatter = NumberFormatter()
numberformatter.numberStyle = .decimal
let formattedNumber = numberformatter.string(from: NSNumber(value: size)) ?? "\(size)"
let byte = "\(formattedNumber)"
return (human: human, byte: byte)
}
// Only process supported files
@@ -603,12 +590,9 @@ func uninstallPearcleaner(appState: AppState, locations: Locations) {
// Get app info for Pearcleaner
let appInfo = AppInfoFetcher.getAppInfo(atPath: Bundle.main.bundleURL)
// appState.appInfo = appInfo!
// Find application files for Pearcleaner
let pathFinder = AppPathFinder(appInfo: appInfo!, appState: appState, locations: locations)
pathFinder.findPaths()
// findPathsForApp(appInfo: appInfo!,appState: appState, locations: locations)
AppPathFinder(appInfo: appInfo!, appState: appState, locations: locations).findPaths()
// Kill Pearcleaner and tell Finder to trash the files
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {