3.8.7 - almost there

This commit is contained in:
Alin
2024-09-18 17:22:51 -06:00
parent e181415d51
commit ef23d0a724
14 changed files with 864 additions and 517 deletions
+5 -11
View File
@@ -12,7 +12,6 @@
C72893052AFD42E600C8C1CD /* DeepLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = C72893042AFD42E600C8C1CD /* DeepLink.swift */; };
C72893122AFD51EA00C8C1CD /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = C72893112AFD51EA00C8C1CD /* main.swift */; };
C72F4B6C2C5DF4ED00B04F60 /* SunburstChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = C72F4B6B2C5DF4ED00B04F60 /* SunburstChart.swift */; };
C736E7652C2DF1C9009EDB6A /* AppPathsFetch-Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = C736E7642C2DF1C9009EDB6A /* AppPathsFetch-Async.swift */; };
C74412C72BBF249000DDFCA8 /* AppPathsFetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C74412C62BBF249000DDFCA8 /* AppPathsFetch.swift */; };
C74D76B72C5C4ABC00748DD1 /* TreeMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = C74D76B62C5C4ABC00748DD1 /* TreeMap.swift */; };
C74D76BD2C5D2DF300748DD1 /* FileSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C74D76B92C5D2DF300748DD1 /* FileSearch.swift */; };
@@ -104,7 +103,6 @@
C728930F2AFD51EA00C8C1CD /* PearcleanerSentinel */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = PearcleanerSentinel; sourceTree = BUILT_PRODUCTS_DIR; };
C72893112AFD51EA00C8C1CD /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; };
C72F4B6B2C5DF4ED00B04F60 /* SunburstChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SunburstChart.swift; sourceTree = "<group>"; };
C736E7642C2DF1C9009EDB6A /* AppPathsFetch-Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppPathsFetch-Async.swift"; sourceTree = "<group>"; };
C74412C62BBF249000DDFCA8 /* AppPathsFetch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPathsFetch.swift; sourceTree = "<group>"; };
C74D76B62C5C4ABC00748DD1 /* TreeMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TreeMap.swift; sourceTree = "<group>"; };
C74D76B92C5D2DF300748DD1 /* FileSearch.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileSearch.swift; sourceTree = "<group>"; };
@@ -183,7 +181,6 @@
C77B90192AF1938F009CC655 /* Logic.swift */,
C7DD49E92BAB4D8100CCBA16 /* AppInfoFetch.swift */,
C74412C62BBF249000DDFCA8 /* AppPathsFetch.swift */,
C736E7642C2DF1C9009EDB6A /* AppPathsFetch-Async.swift */,
C7DD49ED2BAB7F6000CCBA16 /* ReversePathsFetch.swift */,
C77B90172AF19382009CC655 /* AppCommands.swift */,
C77B90152AF19377009CC655 /* AppState.swift */,
@@ -384,7 +381,7 @@
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1530;
LastUpgradeCheck = 1510;
LastUpgradeCheck = 1600;
TargetAttributes = {
C728930E2AFD51EA00C8C1CD = {
CreatedOnToolsVersion = 15.0.1;
@@ -472,7 +469,6 @@
C7045A282B03E71D00376976 /* MiniMode.swift in Sources */,
C7ED86B12BA8A35500F13DE4 /* Interface.swift in Sources */,
C72893052AFD42E600C8C1CD /* DeepLink.swift in Sources */,
C736E7652C2DF1C9009EDB6A /* AppPathsFetch-Async.swift in Sources */,
C71848442B8D2D600046CB13 /* ZombieView.swift in Sources */,
C7DE672B2BA6343D00EB1633 /* MenuBarItem.swift in Sources */,
C76D084A2AF83C6E00D07867 /* General.swift in Sources */,
@@ -561,8 +557,8 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
APP_BUILD = 60;
APP_VERSION = 3.8.6;
APP_BUILD = 61;
APP_VERSION = 3.8.7;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
@@ -631,8 +627,8 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
APP_BUILD = 60;
APP_VERSION = 3.8.6;
APP_BUILD = 61;
APP_VERSION = 3.8.7;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
@@ -693,7 +689,6 @@
C77B90102AF18E2F009CC655 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
APP_BUILD = "$(APP_BUILD)";
APP_VERSION = "$(APP_VERSION)";
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
@@ -733,7 +728,6 @@
C77B90112AF18E2F009CC655 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
APP_BUILD = "$(APP_BUILD)";
APP_VERSION = "$(APP_VERSION)";
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1530"
LastUpgradeVersion = "1600"
wasCreatedForAppExtension = "YES"
version = "2.0">
<BuildAction
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
LastUpgradeVersion = "1600"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
LastUpgradeVersion = "1600"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
LastUpgradeVersion = "1600"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
LastUpgradeVersion = "1600"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
+1 -1
View File
@@ -65,7 +65,7 @@ struct AppCommands: Commands {
undoTrash(appState: appState) {
reloadAppsList(appState: appState, fsm: fsm)
for app in appState.trashedFiles {
AppPathFinder(appInfo: app, appState: appState, locations: locations, undo: true).findPaths()
AppPathFinder(appInfo: app, locations: locations, appState: appState, undo: true).findPaths()
}
}
}
-334
View File
@@ -1,334 +0,0 @@
////
//// AppPathsFetch-Async.swift
//// Pearcleaner
////
//// Created by Alin Lupascu on 6/27/24.
////
//
//import Foundation
//import AppKit
//import SwiftUI
//
//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
// }
//}
//
//class AppPathFinderAsync {
// private var appInfo: AppInfo
// private var appState: AppState
// private var locations: Locations
// private var backgroundRun: Bool
// private var reverseAddon: Bool
// private var undo: 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, undo: Bool = false, completion: @escaping () -> Void = {}) {
// self.appInfo = appInfo
// self.appState = appState
// self.locations = locations
// self.backgroundRun = backgroundRun
// self.reverseAddon = reverseAddon
// self.undo = undo
// self.completion = completion
// }
//
// func findPaths() async {
// await initialURLProcessing()
// await withTaskGroup(of: Void.self) { group in
// for location in self.locations.apps.paths {
// group.addTask {
// await self.processDirectoryLocation(location)
// }
// }
// }
// await withTaskGroup(of: Void.self) { group in
// for location in self.locations.apps.paths {
// group.addTask {
// await self.processFileLocation(location)
// }
// }
// }
// 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 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 {
// 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 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
// }
// }
// }
//
// 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) {
// 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 getAllContainers(bundleURL: URL) async -> [URL] {
// await withCheckedContinuation { continuation in
// var containers: [URL] = []
//
// let bundleIdentifier = Bundle(url: bundleURL)?.bundleIdentifier
//
// guard let containerBundleIdentifier = bundleIdentifier else {
// printOS("Get Containers: No bundle identifier found for the given bundle URL.")
// continuation.resume(returning: containers)
// return
// }
//
// if let groupContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: containerBundleIdentifier) {
// if FileManager.default.fileExists(atPath: groupContainer.path) {
// containers.append(groupContainer)
// }
// } else {
// printOS("Get Containers: Failed to retrieve container URL for bundle identifier: \(containerBundleIdentifier)")
// }
//
// let containersPath = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first?.appendingPathComponent("Containers")
//
// do {
// let containerDirectories = try FileManager.default.contentsOfDirectory(at: containersPath!, includingPropertiesForKeys: nil, options: .skipsHiddenFiles)
//
// let uuidRegex = try NSRegularExpression(pattern: "^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$", options: .caseInsensitive)
//
// for directory in containerDirectories {
// let directoryName = directory.lastPathComponent
//
// if uuidRegex.firstMatch(in: directoryName, options: [], range: NSRange(location: 0, length: directoryName.utf16.count)) != nil {
// let metadataPlistURL = directory.appendingPathComponent(".com.apple.containermanagerd.metadata.plist")
// if let metadataDict = NSDictionary(contentsOf: metadataPlistURL), let applicationBundleID = metadataDict["MCMMetadataIdentifier"] as? String {
// if applicationBundleID == self.appInfo.bundleIdentifier {
// containers.append(directory)
// }
// }
// }
// }
// } catch {
// printOS("Error accessing the Containers directory: \(error)")
// }
//
// continuation.resume(returning: containers)
// }
// }
//
// private func handleOutliers(include: Bool = true) -> [URL] {
// var outliers: [URL] = []
// let bundleIdentifier = self.appInfo.bundleIdentifier.pearFormat()
//
// let matchingConditions = conditions.filter { condition in
// bundleIdentifier.contains(condition.bundle_id)
// }
//
// for condition in matchingConditions {
// if include {
// if let forceIncludes = condition.includeForce {
// for path in forceIncludes {
// outliers.append(path)
// }
// }
// } else {
// if let excludeForce = condition.excludeForce {
// for path in excludeForce {
// outliers.append(path)
// }
// }
// }
// }
//
// return outliers
// }
//
// private func finalizeCollection() async {
// let allContainers = await getAllContainers(bundleURL: self.appInfo.path)
// let outliers = handleOutliers()
// let outliersEx = handleOutliers(include: false)
// var tempCollection: [URL] = []
// collectionAccessQueue.sync {
// tempCollection = self.collection
// }
// tempCollection.append(contentsOf: allContainers)
// tempCollection.append(contentsOf: outliers)
//
// let excludePaths = outliersEx.map { $0.path }
// tempCollection.removeAll { url in
// excludePaths.contains(url.path)
// }
//
// let sortedCollection = tempCollection.map { $0.standardizedFileURL }.sorted(by: { $0.path < $1.path })
// var filteredCollection: [URL] = []
// var previousUrl: URL?
// for url in sortedCollection {
// if let previousUrl = previousUrl, url.path.hasPrefix(previousUrl.path + "/") {
// continue
// }
// 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) {
// let iconData = serializeImage(icon)
// await state.setFileDetails(for: path, size: size.real, icon: iconData)
// } else {
// await state.setFileDetails(for: path, size: size.real, icon: nil)
// }
// }
// await updateAppState(with: sortedCollection)
// }
//
// private func updateAppState(with sortedCollection: [URL]) async {
// let fileSize = await self.state.getFileSize()
// let fileIconData = await self.state.getFileIconData()
// let fileIcons = fileIconData.mapValues { deserializeImage($0) }
// let arch = checkAppBundleArchitecture(at: self.appInfo.path.path)
//
// self.appInfo.fileSize = fileSize
// self.appInfo.fileIcon = fileIcons
// self.appInfo.arch = arch
//
// await MainActor.run {
// if !self.backgroundRun {
// self.appState.appInfo = self.appInfo
// if !self.undo {
// self.appState.selectedItems = Set(sortedCollection)
// }
// }
//
// if self.reverseAddon {
// self.appState.appInfoStore.append(self.appInfo)
// }
//
// self.completion()
// }
// }
//}
//
//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)
//}
+481 -143
View File
@@ -11,42 +11,28 @@ import SwiftUI
import AlinFoundation
class AppPathFinder {
// Shared properties
private var appInfo: AppInfo
private var appState: AppState
private var locations: Locations
private var backgroundRun: Bool
private var undo: Bool
private var completion: () -> Void = {}
private var collection: [URL] = []
private var containerCollection: [URL] = []
private let collectionAccessQueue = DispatchQueue(label: "com.alienator88.Pearcleaner.appPathFinder.collectionAccess")
init(appInfo: AppInfo = .empty, appState: AppState, locations: Locations, backgroundRun: Bool = false, undo: Bool = false, completion: @escaping () -> Void = {}) {
// GUI-specific properties (can be nil for CLI)
private var appState: AppState?
private var undo: Bool = false
private var completion: (() -> Void)?
// Initializer for both CLI and GUI
init(appInfo: AppInfo, locations: Locations, appState: AppState? = nil, undo: Bool = false, completion: (() -> Void)? = nil) {
self.appInfo = appInfo
self.appState = appState
self.locations = locations
self.backgroundRun = backgroundRun
self.appState = appState
self.undo = undo
self.completion = completion
}
func findPaths() {
Task(priority: .background) {
if self.appInfo.webApp {
containerCollection = self.getAllContainers(bundleURL: self.appInfo.path)
self.initialURLProcessing()
self.finalizeCollection()
} else {
containerCollection = self.getAllContainers(bundleURL: self.appInfo.path)
self.initialURLProcessing()
self.collectDirectories()
self.collectFiles()
self.finalizeCollection()
}
}
}
// MARK: - Shared Methods
private func initialURLProcessing() {
if let url = URL(string: self.appInfo.path.absoluteString), !url.path.contains(".Trash") {
let modifiedUrl = url.path.contains("Wrapper") ? url.deletingLastPathComponent().deletingLastPathComponent() : url
@@ -54,18 +40,55 @@ class AppPathFinder {
}
}
private func collectDirectories() {
let dispatchGroup = DispatchGroup()
private func getAllContainers(bundleURL: URL) -> [URL] {
var containers: [URL] = []
for location in self.locations.apps.paths {
dispatchGroup.enter()
DispatchQueue.global(qos: .userInitiated).async {
self.processDirectoryLocation(location)
dispatchGroup.leave()
}
// Extract bundle identifier from bundleURL
let bundleIdentifier = Bundle(url: bundleURL)?.bundleIdentifier
// Ensure the bundleIdentifier is not nil
guard let containerBundleIdentifier = bundleIdentifier else {
printOS("Get Containers: No bundle identifier found for the given bundle URL.")
return containers // Returns whatever was found so far, possibly empty
}
dispatchGroup.wait()
// Get the regular container URL for the extracted bundle identifier
if let groupContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: containerBundleIdentifier) {
if FileManager.default.fileExists(atPath: groupContainer.path) {
containers.append(groupContainer)
}
} else {
printOS("Get Containers: Failed to retrieve container URL for bundle identifier: \(containerBundleIdentifier)")
}
let containersPath = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first?.appendingPathComponent("Containers")
do {
let containerDirectories = try FileManager.default.contentsOfDirectory(at: containersPath!, includingPropertiesForKeys: nil, options: .skipsHiddenFiles)
// Define a regular expression to match UUID format
let uuidRegex = try NSRegularExpression(pattern: "^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$", options: .caseInsensitive)
for directory in containerDirectories {
let directoryName = directory.lastPathComponent
// Check if the directory name matches the UUID pattern
if uuidRegex.firstMatch(in: directoryName, options: [], range: NSRange(location: 0, length: directoryName.utf16.count)) != nil {
// Attempt to read the metadata plist file
let metadataPlistURL = directory.appendingPathComponent(".com.apple.containermanagerd.metadata.plist")
if let metadataDict = NSDictionary(contentsOf: metadataPlistURL), let applicationBundleID = metadataDict["MCMMetadataIdentifier"] as? String {
if applicationBundleID == self.appInfo.bundleIdentifier {
containers.append(directory)
}
}
}
}
} catch {
printOS("Error accessing the Containers directory: \(error)")
}
// Return all found containers
return containers
}
private func processDirectoryLocation(_ location: String) {
@@ -95,20 +118,6 @@ class AppPathFinder {
}
}
private func collectFiles() {
let dispatchGroup = DispatchGroup()
for location in self.locations.apps.paths {
dispatchGroup.enter()
DispatchQueue.global(qos: .userInitiated).async {
self.processFileLocation(location)
dispatchGroup.leave()
}
}
dispatchGroup.wait()
}
private func processFileLocation(_ location: String) {
if let contents = try? FileManager.default.contentsOfDirectory(atPath: location) {
for item in contents {
@@ -126,6 +135,114 @@ class AppPathFinder {
}
}
private func handleOutliers(include: Bool = true) -> [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 include {
// Handle includeForce
if let forceIncludes = condition.includeForce {
for path in forceIncludes {
outliers.append(path)
}
}
} else {
// Handle excludeForce
if let excludeForce = condition.excludeForce {
for path in excludeForce {
outliers.append(path)
}
}
}
}
return outliers
}
// MARK: - findPaths Methods
func findPaths() {
Task(priority: .background) {
if self.appInfo.webApp {
containerCollection = getAllContainers(bundleURL: self.appInfo.path)
self.initialURLProcessing()
self.finalizeCollection()
} else {
containerCollection = getAllContainers(bundleURL: self.appInfo.path)
self.initialURLProcessing()
self.collectDirectories()
self.collectFiles()
self.finalizeCollection()
}
}
}
func findPathsCLI() -> Set<URL> {
if self.appInfo.webApp {
containerCollection = getAllContainers(bundleURL: self.appInfo.path)
self.initialURLProcessing()
return finalizeCollectionCLI() // Return the collected paths
} else {
containerCollection = getAllContainers(bundleURL: self.appInfo.path)
self.initialURLProcessing()
self.collectDirectoriesCLI() // Synchronous version
self.collectFilesCLI() // Synchronous version
return finalizeCollectionCLI() // Return the collected paths
}
}
// MARK: - Unique Methods
private func collectDirectories() {
let dispatchGroup = DispatchGroup()
for location in self.locations.apps.paths {
dispatchGroup.enter()
DispatchQueue.global(qos: .userInitiated).async {
self.processDirectoryLocation(location)
dispatchGroup.leave()
}
}
dispatchGroup.wait()
}
private func collectFiles() {
let dispatchGroup = DispatchGroup()
for location in self.locations.apps.paths {
dispatchGroup.enter()
DispatchQueue.global(qos: .userInitiated).async {
self.processFileLocation(location)
dispatchGroup.leave()
}
}
dispatchGroup.wait()
}
private func collectDirectoriesCLI() {
for location in self.locations.apps.paths {
processDirectoryLocation(location)
}
}
private func collectFilesCLI() {
for location in self.locations.apps.paths {
processFileLocation(location)
}
}
private func shouldSkipItem(_ itemL: String, at itemURL: URL) -> Bool {
@@ -185,93 +302,9 @@ class AppPathFinder {
}
private func getAllContainers(bundleURL: URL) -> [URL] {
var containers: [URL] = []
// Extract bundle identifier from bundleURL
let bundleIdentifier = Bundle(url: bundleURL)?.bundleIdentifier
// Ensure the bundleIdentifier is not nil
guard let containerBundleIdentifier = bundleIdentifier else {
printOS("Get Containers: No bundle identifier found for the given bundle URL.")
return containers // Returns whatever was found so far, possibly empty
}
// Get the regular container URL for the extracted bundle identifier
if let groupContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: containerBundleIdentifier) {
if FileManager.default.fileExists(atPath: groupContainer.path) {
containers.append(groupContainer)
}
} else {
printOS("Get Containers: Failed to retrieve container URL for bundle identifier: \(containerBundleIdentifier)")
}
let containersPath = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first?.appendingPathComponent("Containers")
do {
let containerDirectories = try FileManager.default.contentsOfDirectory(at: containersPath!, includingPropertiesForKeys: nil, options: .skipsHiddenFiles)
// Define a regular expression to match UUID format
let uuidRegex = try NSRegularExpression(pattern: "^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$", options: .caseInsensitive)
for directory in containerDirectories {
let directoryName = directory.lastPathComponent
// Check if the directory name matches the UUID pattern
if uuidRegex.firstMatch(in: directoryName, options: [], range: NSRange(location: 0, length: directoryName.utf16.count)) != nil {
// Attempt to read the metadata plist file
let metadataPlistURL = directory.appendingPathComponent(".com.apple.containermanagerd.metadata.plist")
if let metadataDict = NSDictionary(contentsOf: metadataPlistURL), let applicationBundleID = metadataDict["MCMMetadataIdentifier"] as? String {
if applicationBundleID == self.appInfo.bundleIdentifier {
containers.append(directory)
}
}
}
}
} catch {
printOS("Error accessing the Containers directory: \(error)")
}
// Return all found containers
return containers
}
private func handleOutliers(include: Bool = true) -> [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 include {
// Handle includeForce
if let forceIncludes = condition.includeForce {
for path in forceIncludes {
outliers.append(path)
}
}
} else {
// Handle excludeForce
if let excludeForce = condition.excludeForce {
for path in excludeForce {
outliers.append(path)
}
}
}
}
return outliers
}
private func finalizeCollection() {
DispatchQueue.global(qos: .userInitiated).async {
// let allContainers = self.getAllContainers(bundleURL: self.appInfo.path)
let outliers = self.handleOutliers()
let outliersEx = self.handleOutliers(include: false)
var tempCollection: [URL] = []
@@ -302,10 +335,48 @@ class AppPathFinder {
}
self.handlePostProcessing(sortedCollection: filteredCollection)
}
}
private func finalizeCollectionCLI() -> Set<URL> {
let outliers = handleOutliers()
let outliersEx = handleOutliers(include: false)
var tempCollection: [URL] = []
self.collectionAccessQueue.sync {
tempCollection = self.collection
}
tempCollection.append(contentsOf: self.containerCollection)
tempCollection.append(contentsOf: outliers)
// Remove excluded paths
let excludePaths = outliersEx.map { $0.path }
tempCollection.removeAll { url in
excludePaths.contains(url.path)
}
// Sort and filter subdirectories
let sortedCollection = tempCollection.map { $0.standardizedFileURL }.sorted(by: { $0.path < $1.path })
var filteredCollection: [URL] = []
var previousUrl: URL?
for url in sortedCollection {
if let previousUrl = previousUrl, url.path.hasPrefix(previousUrl.path + "/") {
continue
}
filteredCollection.append(url)
previousUrl = url
}
// Remove Trash paths if necessary
if filteredCollection.count == 1, let firstURL = filteredCollection.first, firstURL.path.contains(".Trash") {
filteredCollection.removeAll()
}
return Set(filteredCollection)
}
private func handlePostProcessing(sortedCollection: [URL]) {
// Calculate file details (sizes and icons), update app state, and call completion
var fileSize: [URL: Int64] = [:]
@@ -321,31 +392,298 @@ class AppPathFinder {
let arch = checkAppBundleArchitecture(at: self.appInfo.path.path)
DispatchQueue.main.async {
var updatedCollection = sortedCollection
if updatedCollection.count == 1, let firstURL = updatedCollection.first, firstURL.path.contains(".Trash") {
updatedCollection.removeAll()
}
DispatchQueue.main.async {
// Update appInfo and appState with the new values
self.appInfo.fileSize = fileSize
self.appInfo.fileSizeLogical = fileSizeLogical
self.appInfo.fileIcon = fileIcon
self.appInfo.arch = arch
if !self.backgroundRun {
self.appState.appInfo = self.appInfo
self.appState?.appInfo = self.appInfo
if !self.undo {
self.appState.selectedItems = Set(updatedCollection)
self.appState?.selectedItems = Set(updatedCollection)
}
self.completion?()
}
}
}
// Append object to store if running reverse search with empty store
// if self.reverseAddon {
// self.appState.appInfoStore.append(self.appInfo)
// CLI // =========================================================================================================================
//class AppPathFinderCLI {
// private var appInfo: AppInfo
// private var locations: Locations
// private var collection: [URL] = []
// private var containerCollection: [URL] = []
// private let collectionAccessQueue = DispatchQueue(label: "com.alienator88.Pearcleaner.appPathFinderCLI.collectionAccess")
//
// init(appInfo: AppInfo = .empty, locations: Locations) {
// self.appInfo = appInfo
// self.locations = locations
// }
//
// // Make this function synchronous for the CLI
// func findPaths() -> Set<URL> {
// if self.appInfo.webApp {
// containerCollection = getAllContainers(bundleURL: self.appInfo.path)
// self.initialURLProcessing()
// return finalizeCollection() // Return the collected paths
// } else {
// containerCollection = getAllContainers(bundleURL: self.appInfo.path)
// self.initialURLProcessing()
// self.collectDirectories() // Synchronous version
// self.collectFiles() // Synchronous version
// return finalizeCollection() // Return the collected paths
// }
// }
//
// private func initialURLProcessing() {
// if let url = URL(string: self.appInfo.path.absoluteString), !url.path.contains(".Trash") {
// let modifiedUrl = url.path.contains("Wrapper") ? url.deletingLastPathComponent().deletingLastPathComponent() : url
// self.collection.append(modifiedUrl)
// }
// }
//
// 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 processDirectoryLocation(_ location: String) {
// if 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)
// }
// }
// }
// }
// }
// }
//
// 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 nameLFiltered = nameL.filter { $0.isLetter }
//
// let nameP = self.appInfo.path.lastPathComponent.replacingOccurrences(of: ".app", with: "")
//
// for condition in conditions {
// if bundleIdentifierL.contains(condition.bundle_id) {
// // Exclude keywords
// let hasExcludeKeyword = condition.exclude.contains(where: itemL.contains)
// if hasExcludeKeyword {
// return false
// }
// // Include keywords
// let hasIncludeKeyword = condition.include.contains(where: itemL.contains)
// if hasIncludeKeyword {
// 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) || (nameLFiltered.count > 3 && itemL.contains(nameLFiltered)))
//
// }
//
// // Make collectDirectories synchronous
// private func collectDirectories() {
// for location in self.locations.apps.paths {
// self.processDirectoryLocation(location) // Synchronous call
// }
// }
//
// private func processFileLocation(_ location: String) {
// if 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)
// }
// }
// }
// }
// }
//
// private func getAllContainers(bundleURL: URL) -> [URL] {
// var containers: [URL] = []
//
// // Extract bundle identifier from bundleURL
// let bundleIdentifier = Bundle(url: bundleURL)?.bundleIdentifier
//
// // Ensure the bundleIdentifier is not nil
// guard let containerBundleIdentifier = bundleIdentifier else {
// printOS("Get Containers: No bundle identifier found for the given bundle URL.")
// return containers // Returns whatever was found so far, possibly empty
// }
//
// // Get the regular container URL for the extracted bundle identifier
// if let groupContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: containerBundleIdentifier) {
// if FileManager.default.fileExists(atPath: groupContainer.path) {
// containers.append(groupContainer)
// }
// } else {
// printOS("Get Containers: Failed to retrieve container URL for bundle identifier: \(containerBundleIdentifier)")
// }
//
// let containersPath = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first?.appendingPathComponent("Containers")
//
// do {
// let containerDirectories = try FileManager.default.contentsOfDirectory(at: containersPath!, includingPropertiesForKeys: nil, options: .skipsHiddenFiles)
//
// // Define a regular expression to match UUID format
// let uuidRegex = try NSRegularExpression(pattern: "^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$", options: .caseInsensitive)
//
// for directory in containerDirectories {
// let directoryName = directory.lastPathComponent
//
// // Check if the directory name matches the UUID pattern
// if uuidRegex.firstMatch(in: directoryName, options: [], range: NSRange(location: 0, length: directoryName.utf16.count)) != nil {
// // Attempt to read the metadata plist file
// let metadataPlistURL = directory.appendingPathComponent(".com.apple.containermanagerd.metadata.plist")
// if let metadataDict = NSDictionary(contentsOf: metadataPlistURL), let applicationBundleID = metadataDict["MCMMetadataIdentifier"] as? String {
// if applicationBundleID == self.appInfo.bundleIdentifier {
// containers.append(directory)
// }
// }
// }
// }
// } catch {
// printOS("Error accessing the Containers directory: \(error)")
// }
//
// // Return all found containers
// return containers
// }
//
// // Make collectFiles synchronous
// private func collectFiles() {
// for location in self.locations.apps.paths {
// self.processFileLocation(location) // Synchronous call
// }
// }
//
// private func handleOutliers(include: Bool = true) -> [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 include {
// // Handle includeForce
// if let forceIncludes = condition.includeForce {
// for path in forceIncludes {
// outliers.append(path)
// }
// }
// } else {
// // Handle excludeForce
// if let excludeForce = condition.excludeForce {
// for path in excludeForce {
// outliers.append(path)
// }
// }
// }
//
// }
//
// return outliers
// }
//
// // No need to dispatch asynchronously here
// private func finalizeCollection() -> Set<URL> {
// let outliers = self.handleOutliers()
// let outliersEx = self.handleOutliers(include: false)
// var tempCollection: [URL] = []
// self.collectionAccessQueue.sync {
// tempCollection = self.collection
// }
// tempCollection.append(contentsOf: self.containerCollection)
// tempCollection.append(contentsOf: outliers)
//
// let excludePaths = outliersEx.map { $0.path }
// tempCollection.removeAll { url in
// excludePaths.contains(url.path)
// }
//
// let sortedCollection = tempCollection.map { $0.standardizedFileURL }.sorted(by: { $0.path < $1.path })
// var filteredCollection: [URL] = []
// var previousUrl: URL?
//
// for url in sortedCollection {
// if let previousUrl = previousUrl, url.path.hasPrefix(previousUrl.path + "/") {
// continue
// }
// filteredCollection.append(url)
// previousUrl = url
// }
//
// // Handle Trash directory case
// var updatedCollection = filteredCollection
// if updatedCollection.count == 1, let firstURL = updatedCollection.first, firstURL.path.contains(".Trash") {
// // If only one item exists and it's in the Trash, remove it
// updatedCollection.removeAll()
// }
//
// return Set(updatedCollection)
// }
//
//}
self.completion()
}
}
}
+36 -1
View File
@@ -145,7 +145,7 @@ func showAppInFiles(appInfo: AppInfo, appState: AppState, locations: Locations,
appState.showProgress = true
// Initialize the path finder and execute its search.
AppPathFinder(appInfo: appInfo, appState: appState, locations: locations) {
AppPathFinder(appInfo: appInfo, locations: locations, appState: appState) {
updateOnMain {
// Update the progress indicator on the main thread once the search completes.
appState.showProgress = false
@@ -210,7 +210,42 @@ func moveFilesToTrash(appState: AppState, at fileURLs: [URL], completion: @escap
}
func moveFilesToTrashCLI(at fileURLs: [URL]) -> Bool {
// Stop Sentinel FileWatcher momentarily to ignore .app bundle being sent to Trash
sendStopNotificationFW()
// Create the AppleScript for moving files to the Trash
let posixFiles = fileURLs.map { item in
return "POSIX file \"\(item.path)\"" + (item == fileURLs.last ? "" : ", ")}.joined()
let scriptSource = """
tell application \"Finder\" to delete { \(posixFiles) }
"""
var error: NSDictionary?
if let scriptObject = NSAppleScript(source: scriptSource) {
let output: NSAppleEventDescriptor = scriptObject.executeAndReturnError(&error)
// Handle any AppleScript errors
if let error = error {
print("Trash Error: \(error)") // Synchronous error reporting
return false // Indicate failure
}
// Check if output is null, indicating the user canceled the operation
if output.descriptorType == typeNull {
print("Trash Error: operation canceled by the user") // Synchronous cancellation reporting
return false // Indicate failure due to cancellation
}
// Process output if it exists
if let outputString = output.stringValue {
print("Trash: \(outputString)")
}
}
return true // Indicate success
}
// Undo trash action
+206 -3
View File
@@ -56,13 +56,215 @@ func findAndSetWindowFrame(named titles: [String], windowSettings: WindowSetting
}
// Process CLI // ========================================================================================================
func processCLI(arguments: [String], appState: AppState, locations: Locations) {
let options = Array(arguments.dropFirst()) // Remove the first argument (binary path)
// Private function to list files for uninstall, using the provided path
func listFiles(at path: String) {
// Convert the provided string path to a URL
let url = URL(fileURLWithPath: path)
// print("[BETA] Pearcleaner CLI | List Files:\n")
// Fetch the app info and safely unwrap
guard let appInfo = AppInfoFetcher.getAppInfo(atPath: url) else {
print("Error: Invalid path or unable to fetch app info at path: \(path)")
exit(1) // Exit with non-zero code to indicate failure
}
// Use the AppPathFinderCLI to find paths synchronously
let appPathFinder = AppPathFinder(appInfo: appInfo, locations: locations)
// Call findPaths to get the Set of URLs
let foundPaths = appPathFinder.findPathsCLI()
// Print each path in the Set to the console
for path in foundPaths {
print(path.path)
}
}
// Private function to uninstall the application bundle at a given path
func uninstallApp(at path: String) {
// Convert the provided string path to a URL
let url = URL(fileURLWithPath: path)
print("[BETA] Pearcleaner CLI | Uninstall Application:\n")
// Fetch the app info and safely unwrap
guard let appInfo = AppInfoFetcher.getAppInfo(atPath: url) else {
print("Error: Invalid path or unable to fetch app info at path: \(path)")
exit(1) // Exit with non-zero code to indicate failure
}
killApp(appId: appInfo.bundleIdentifier) {
let success = moveFilesToTrashCLI(at: [appInfo.path])
if success {
print("Application moved to the trash successfully.")
exit(0)
} else {
print("Failed to move application to trash.")
exit(1)
}
}
}
// Private function to uninstall the application and all related files at a given path
func uninstallAll(at path: String) {
// Convert the provided string path to a URL
let url = URL(fileURLWithPath: path)
print("[BETA] Pearcleaner CLI | Uninstall Application + Related Files:\n")
// Fetch the app info and safely unwrap
guard let appInfo = AppInfoFetcher.getAppInfo(atPath: url) else {
print("Error: Invalid path or unable to fetch app info at path: \(path)")
exit(1) // Exit with non-zero code to indicate failure
}
// Use the AppPathFinderCLI to find paths synchronously
let appPathFinder = AppPathFinder(appInfo: appInfo, locations: locations)
// Call findPaths to get the Set of URLs
let foundPaths = appPathFinder.findPathsCLI()
killApp(appId: appInfo.bundleIdentifier) {
let success = moveFilesToTrashCLI(at: Array(foundPaths))
if success {
print("The following files have been moved to the trash successfully:\n")
// Print each path in the Set to the console
for path in foundPaths {
print(path.path)
}
exit(0)
} else {
print("Failed to move application and related files to trash.")
exit(1)
}
}
}
// Handle help option (-h or --help)
if options.contains("-h") || options.contains("--help") {
displayHelp()
exit(0)
}
// Handle --list or -l option with a path argument
if let listIndex = options.firstIndex(where: { $0 == "--list" || $0 == "-l" }), listIndex + 1 < options.count {
let path = options[listIndex + 1] // Path provided after --list or -l
listFiles(at: path)
exit(0)
}
// Handle --uninstall or -u option with a path argument
if let uninstallIndex = options.firstIndex(where: { $0 == "--uninstall" || $0 == "-u" }), uninstallIndex + 1 < options.count {
let path = options[uninstallIndex + 1] // Path provided after --uninstall or -u
uninstallApp(at: path)
exit(0)
}
// Handle --uninstall-all or -ua option with a path argument
if let uninstallAllIndex = options.firstIndex(where: { $0 == "--uninstall-all" || $0 == "-ua" }), uninstallAllIndex + 1 < options.count {
let path = options[uninstallAllIndex + 1] // Path provided after --uninstall-all or -ua
uninstallAll(at: path)
exit(0)
}
// If no valid option was provided, show the help menu by default
displayHelp()
exit(0)
}
// Private function to display help message
func displayHelp() {
print("""
[BETA] Pearcleaner CLI | Usage:
--list <path>, -l <path> Find application files available for uninstall at the specified path
--uninstall <path>, -u <path> Remove only the application bundle at the specified path
--uninstall-all <path>, -ua <path> Remove the application bundle and all related files at the specified path
--help, -h Show this help message
""")
}
// Check if pearcleaner symlink exists
func checkCLISymlink() -> Bool {
let filePath = "/usr/local/bin/pearcleaner"
let fileManager = FileManager.default
// Check if the file exists at the given path
return fileManager.fileExists(atPath: filePath)
}
// Install/uninstall symlink for CLI
func manageSymlink(install: Bool) {
// Get the current running application's bundle binary path
guard let appPath = Bundle.main.executablePath else {
printOS("Error: Unable to get the executable path.")
return
}
// Path where the symlink should be created
let symlinkPath = "/usr/local/bin/pearcleaner"
// Check if the symlink already exists
let symlinkExists = checkCLISymlink()
// If we are installing the symlink and it already exists, skip creating it
if install && symlinkExists {
printOS("Symlink already exists at \(symlinkPath). No action needed.")
return
}
// If we are uninstalling the symlink and it doesn't exist, skip removing it
if !install && !symlinkExists {
printOS("Symlink does not exist at \(symlinkPath). No action needed.")
return
}
// Create AppleScript commands for installing or uninstalling the symlink
let script: String
if install {
// AppleScript to create a symlink with admin privileges
script = """
do shell script "ln -s '\(appPath)' '\(symlinkPath)'" with administrator privileges
"""
} else {
// AppleScript to remove the symlink with admin privileges
script = """
do shell script "rm '\(symlinkPath)'" with administrator privileges
"""
}
// Execute the AppleScript
var error: NSDictionary?
if let scriptObject = NSAppleScript(source: script) {
scriptObject.executeAndReturnError(&error)
if let error = error {
printOS("AppleScript Error: \(error)")
} else {
if install {
printOS("Symlink created successfully at \(symlinkPath).")
} else {
printOS("Symlink removed successfully from \(symlinkPath).")
}
}
} else {
printOS("Error: Unable to create the AppleScript object.")
}
}
// Brew cleanup
func caskCleanup(app: String) {
Task(priority: .high) {
let formattedApp = app.lowercased().replacingOccurrences(of: " ", with: "-")
print(formattedApp)
#if arch(x86_64)
let cmd = "/usr/local/bin/brew"
@@ -85,6 +287,8 @@ func caskCleanup(app: String) {
end tell
"""
print(script)
var error: NSDictionary?
if let appleScript = NSAppleScript(source: script) {
appleScript.executeAndReturnError(&error)
@@ -98,7 +302,6 @@ func caskCleanup(app: String) {
// Print list of files locally
func saveURLsToFile(urls: Set<URL>, appState: AppState, copy: Bool = false) {
@@ -426,7 +629,7 @@ func uninstallPearcleaner(appState: AppState, locations: Locations) {
let appInfo = AppInfoFetcher.getAppInfo(atPath: Bundle.main.bundleURL)
// Find application files for Pearcleaner
AppPathFinder(appInfo: appInfo!, appState: appState, locations: locations, completion: {
AppPathFinder(appInfo: appInfo!, locations: locations, appState: appState, completion: {
// Kill Pearcleaner and tell Finder to trash the files
let selectedItemsArray = Array(appState.selectedItems).filter { !$0.path.contains(".Trash") }
let posixFiles = selectedItemsArray.map { item in
+17
View File
@@ -29,6 +29,23 @@ struct PearcleanerApp: App {
@State private var showPopover: Bool = false
let conditionManager = ConditionManager.shared
init() {
let arguments = CommandLine.arguments
let filteredArguments = arguments.filter { !["-NSDocumentRevisionsDebugMode", "YES"].contains($0) }
let isRunningInTerminal = isatty(STDIN_FILENO) != 0
// If running from terminal and no arguments are provided
if isRunningInTerminal && arguments.count == 1 {
displayHelp()
exit(0) // Exit without launching the GUI
}
// The first argument is always the binary path, so check if there are more than 1 arguments
if filteredArguments.count > 1 {
// Process the CLI options
processCLI(arguments: arguments, appState: appState, locations: locations)
}
}
var body: some Scene {
WindowGroup {
+89 -7
View File
@@ -9,6 +9,7 @@ import Foundation
import SwiftUI
import FinderSync
import AlinFoundation
import UniformTypeIdentifiers
struct GeneralSettingsTab: View {
@EnvironmentObject var appState: AppState
@@ -20,7 +21,9 @@ struct GeneralSettingsTab: View {
@AppStorage("settings.general.confirmAlert") private var confirmAlert: Bool = false
@AppStorage("settings.general.selectedSort") var selectedSortAlpha: Bool = true
@AppStorage("settings.general.sizeType") var sizeType: String = "Real"
// @AppStorage("settings.general.brewTerminal") var brewTerm: String = "Terminal"
@State private var isResetting = false
@State private var isCLISymlinked = false
var body: some View {
VStack() {
@@ -39,13 +42,33 @@ struct GeneralSettingsTab: View {
.frame(width: 20, height: 20)
.padding(.trailing)
.foregroundStyle(.primary.opacity(0.5))
VStack(alignment: .leading, spacing: 5) {
VStack(alignment: .leading, spacing: 0) {
HStack(spacing: 0) {
Text("\(brew ? "Homebrew cleanup is enabled" : "Homebrew cleanup is disabled")")
.font(.callout)
.foregroundStyle(.primary.opacity(0.5))
InfoButton(text: "When homebrew cleanup is enabled, Pearcleaner will check if the app you are removing was installed via homebrew and launch Terminal.app to execute a brew uninstall and cleanup command to let homebrew know that the app is removed. This way your homebrew list will be synced up correctly and caching will be removed. Terminal.app is required since some apps need sudo permissions to remove services and files placed in system folders. Since other terminal apps don't support applescript and/or the 'do script' command, I opted to use the default macOS Terminal app for this.\n\nNOTE: If you undo the file delete with CMD+Z, the files will be put back but homebrew will not be aware of it. To get the homebrew list back in sync you'd need to run:\n brew install APPNAME --force")
}
InfoButton(text: "When homebrew cleanup is enabled, Pearcleaner will check if the app you are removing was installed via homebrew and launch Terminal.app to execute a brew uninstall and cleanup command to let homebrew know that the app is removed. This way your homebrew list will be synced up correctly and caching will be removed. Terminal is required since some apps need sudo permissions to remove services and files placed in system folders.\n\nNOTE: If you undo the file delete with CMD+Z, the files will be put back but homebrew will not be aware of it. To get the homebrew list back in sync you'd need to run:\n brew install APPNAME --force")
// HStack(spacing: 0) {
// Text("Cleanup application: \(brewTerm)")
// .font(.footnote)
// .foregroundStyle(.primary.opacity(0.4))
//
// Button("") {
// selectBrewTerm()
// }
// .buttonStyle(SimpleButtonStyle(icon: "gear", help: "Set terminal application, right click to Reset", size: 14))
// .contextMenu{
// Button("Reset") {
// brewTerm = "Terminal"
// }
// }
// }
}
Spacer()
Toggle(isOn: $brew, label: {
@@ -116,12 +139,12 @@ struct GeneralSettingsTab: View {
.font(.callout)
.foregroundStyle(.primary.opacity(0.5))
}
InfoButton(text: "When searching for app files or leftover files, the list will be sorted either alphabetically or by size(large to small)")
// InfoButton(text: "When searching for app files or leftover files, the list will be sorted either alphabetically or by size(large to small)")
Spacer()
Picker("", selection: $selectedSortAlpha) {
Text("Alpha")
Text("Alphabetical")
.tag(true)
Text("Size")
Text("File Size")
.tag(false)
}
.buttonStyle(.borderless)
@@ -237,6 +260,50 @@ struct GeneralSettingsTab: View {
.padding(5)
.padding(.leading)
// === CLI =============================================================================================
Divider()
.padding()
HStack() {
Text("Command Line").font(.title2)
Spacer()
}
.padding(.leading)
HStack(spacing: 0) {
Image(systemName: "terminal")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(.trailing)
.foregroundStyle(isCLISymlinked ? .green : .red)
.saturation(themeManager.displayMode.colorScheme == .dark ? 0.8 : 1)
Text(isCLISymlinked ? "Pearcleaner CLI is installed" : "Pearcleaner CLI is **not** installed")
.font(.callout)
.foregroundStyle(.primary.opacity(0.5))
InfoButton(text: "Enabling the CLI will allow you to execute Pearcleaner actions from the Terminal. This will add pearcleaner command into /usr/local/bin so it's available directly from your PATH environment variable. Try it after enabling:\n\n > pearcleaner --help")
Spacer()
Toggle(isOn: $isCLISymlinked, label: {
})
.toggleStyle(.switch)
.onChange(of: isCLISymlinked) { newValue in
if newValue {
manageSymlink(install: true)
} else {
manageSymlink(install: false)
}
}
}
.padding(5)
.padding(.leading)
// === Reset Settings =============================================================================================
HStack() {
@@ -250,16 +317,17 @@ struct GeneralSettingsTab: View {
Spacer()
}
.padding(.vertical, 5)
.padding(.vertical, 10)
Spacer()
}
.onAppear {
appState.updateExtensionStatus()
isCLISymlinked = checkCLISymlink()
}
.padding(20)
.frame(width: 500, height: 520)
.frame(width: 500, height: 640)
}
@@ -273,6 +341,20 @@ struct GeneralSettingsTab: View {
}
}
// private func selectBrewTerm() {
// let dialog = NSOpenPanel()
// dialog.title = "Select a Terminal application"
// dialog.allowedContentTypes = [UTType("com.apple.application-bundle")!] // Limit to .app bundles
// dialog.allowsMultipleSelection = false
// dialog.canChooseFiles = true
// dialog.canChooseDirectories = false
//
// if dialog.runModal() == .OK, let url = dialog.url {
// let appNameWithExtension = url.lastPathComponent
// brewTerm = appNameWithExtension.replacingOccurrences(of: ".app", with: "")
// }
// }
}
+16 -4
View File
@@ -44,15 +44,27 @@ struct SectionView: View {
var apps: [AppInfo]
@Binding var search: String
@Binding var showPopover: Bool
@State private var showItems: Bool = true
@AppStorage("settings.interface.animationEnabled") private var animationEnabled: Bool = true
var body: some View {
LazyVStack(spacing: 0) {
Header(title: title, count: count, showPopover: $showPopover)
.padding(.leading, 5)
.onTapGesture {
withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) {
showItems.toggle()
}
}
if showItems {
ForEach(apps, id: \.self) { appInfo in
AppListItems(search: $search, showPopover: $showPopover, appInfo: appInfo)
.transition(.opacity)
}
}
}
}
}
@@ -66,7 +78,7 @@ struct Header: View {
@EnvironmentObject var fsm: FolderSettingsManager
@Binding var showPopover: Bool
@AppStorage("settings.general.glass") private var glass: Bool = true
@AppStorage("settings.general.selectedSortAppsList") var selectedSortAlpha: Bool = true
// @AppStorage("settings.general.selectedSortAppsList") var selectedSortAlpha: Bool = true
var body: some View {
@@ -87,9 +99,9 @@ struct Header: View {
.frame(minHeight: 20)
.padding(5)
.help("Click header to change sorting order")
.onTapGesture {
selectedSortAlpha.toggle()
}
// .onTapGesture {
// selectedSortAlpha.toggle()
// }
}
}