This commit is contained in:
Alin
2024-09-23 17:19:29 -06:00
parent 3c50edc53c
commit 04862bc1d7
9 changed files with 219 additions and 54 deletions
+1 -1
View File
@@ -47,7 +47,7 @@ class AppInfoFetcher {
return nil
}
let appName = wrapped ? path.deletingLastPathComponent().deletingLastPathComponent().deletingPathExtension().lastPathComponent.capitalizingFirstLetter() : path.deletingPathExtension().lastPathComponent.capitalizingFirstLetter()
let appName = wrapped ? path.deletingLastPathComponent().deletingLastPathComponent().deletingPathExtension().lastPathComponent.capitalizingFirstLetter() : path.localizedName().capitalizingFirstLetter()
let appVersion = (bundle.infoDictionary?["CFBundleShortVersionString"] as? String)?.isEmpty ?? true
? bundle.infoDictionary?["CFBundleVersion"] as? String ?? ""
+1
View File
@@ -27,6 +27,7 @@ class AppState: ObservableObject {
@Published var finderExtensionEnabled: Bool = false
@Published var showUninstallAlert: Bool = false
@Published var oneShotMode: Bool = false
@Published var sentinelMode: Bool = false
@Published var showConditionBuilder: Bool = false
var operationQueueLeftover = OperationQueue()
+9
View File
@@ -22,6 +22,7 @@ class DeeplinkManager {
static let scheme = "pear"
static let host = "com.alienator88.Pearcleaner"
static let query = "path"
static let brew = "brew"
}
func manage(url: URL, appState: AppState, locations: Locations) {
@@ -37,11 +38,19 @@ class DeeplinkManager {
let pathURL = URL(fileURLWithPath: path)
let appInfo = AppInfoFetcher.getAppInfo(atPath: pathURL)
showAppInFiles(appInfo: appInfo!, appState: appState, locations: locations, showPopover: $showPopover)
// Check if the 'trash' parameter is set to true
if let brewValue = queryItems.first(where: { $0.name == DeepLinkConstants.brew })?.value, brewValue == "true" {
updateOnMain {
appState.sentinelMode = true
}
}
if oneShotMode {
updateOnMain {
appState.oneShotMode = true
}
}
} else {
printOS("No path query parameter found in the URL")
}
+1 -1
View File
@@ -11,7 +11,7 @@ import AlinFoundation
// Get all apps from /Applications and ~/Applications
func getSortedApps(paths: [String], appState: AppState) -> [AppInfo] {
func getSortedApps(paths: [String]) -> [AppInfo] {
let fileManager = FileManager.default
var apps: [URL] = []
+14 -6
View File
@@ -10,7 +10,7 @@ import AppKit
import AlinFoundation
class ReversePathsSearcher {
private let appState: AppState
private let appState: AppState?
private let locations: Locations
private let fsm: FolderSettingsManager
private let fileManager = FileManager.default
@@ -21,7 +21,7 @@ class ReversePathsSearcher {
private let dispatchGroup = DispatchGroup()
private let sortedApps: [AppInfo]
init(appState: AppState, locations: Locations, fsm: FolderSettingsManager, sortedApps: [AppInfo]) {
init(appState: AppState? = nil, locations: Locations, fsm: FolderSettingsManager, sortedApps: [AppInfo]) {
self.appState = appState
self.locations = locations
self.fsm = fsm
@@ -37,14 +37,22 @@ class ReversePathsSearcher {
}
}
func reversePathsSearchCLI() -> [URL] {
self.processLocationsCLI()
return collection
}
private func processLocations() {
for location in locations.reverse.paths where fileManager.fileExists(atPath: location) {
dispatchGroup.enter()
processLocation(location)
dispatchGroup.leave()
}
}
private func processLocationsCLI() {
for location in locations.reverse.paths where fileManager.fileExists(atPath: location) {
processLocation(location)
}
}
@@ -132,8 +140,8 @@ class ReversePathsSearcher {
updatedZombieFile.fileSize = self.fileSize
updatedZombieFile.fileSizeLogical = self.fileSizeLogical
updatedZombieFile.fileIcon = self.fileIcon
self.appState.zombieFile = updatedZombieFile
self.appState.showProgress = false
self.appState?.zombieFile = updatedZombieFile
self.appState?.showProgress = false
}
}
+184 -38
View File
@@ -14,7 +14,7 @@ import AppKit
func reloadAppsList(appState: AppState, fsm: FolderSettingsManager) {
appState.reload = true
updateOnBackground {
let sortedApps = getSortedApps(paths: fsm.folderPaths, appState: appState)
let sortedApps = getSortedApps(paths: fsm.folderPaths)
// Update UI on the main thread
updateOnMain {
appState.sortedApps = sortedApps
@@ -59,7 +59,7 @@ func findAndSetWindowFrame(named titles: [String], windowSettings: WindowSetting
// Process CLI // ========================================================================================================
func processCLI(arguments: [String], appState: AppState, locations: Locations) {
func processCLI(arguments: [String], appState: AppState, locations: Locations, fsm: FolderSettingsManager) {
let options = Array(arguments.dropFirst()) // Remove the first argument (binary path)
// Private function to list files for uninstall, using the provided path
@@ -67,11 +67,11 @@ func processCLI(arguments: [String], appState: AppState, locations: Locations) {
// Convert the provided string path to a URL
let url = URL(fileURLWithPath: path)
// print("[BETA] Pearcleaner CLI | List Files:\n")
print("[BETA] Pearcleaner CLI | List Application 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)")
print("Error: Invalid path or unable to fetch app info at path: \(path)\n")
exit(1) // Exit with non-zero code to indicate failure
}
@@ -85,6 +85,27 @@ func processCLI(arguments: [String], appState: AppState, locations: Locations) {
for path in foundPaths {
print(path.path)
}
print("\nFound \(foundPaths.count) application files.\n")
}
// Private function to list leftover files for uninstall, using the provided path
func listLeftoverFiles() {
print("[BETA] Pearcleaner CLI | List Leftover Files:\n")
// Get installed apps for filtering
let sortedApps = getSortedApps(paths: fsm.folderPaths)
// Find leftover files
let foundPaths = ReversePathsSearcher(locations: locations, fsm: fsm, sortedApps: sortedApps)
.reversePathsSearchCLI()
// Print each path in the array to the console
for path in foundPaths {
print(path.path)
}
print("\nFound \(foundPaths.count) leftover files.\n")
}
// Private function to uninstall the application bundle at a given path
@@ -95,17 +116,17 @@ func processCLI(arguments: [String], appState: AppState, locations: Locations) {
// 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)")
print("Error: Invalid path or unable to fetch app info at path: \(path)\n")
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.")
print("Application moved to the trash successfully.\n")
exit(0)
} else {
print("Failed to move application to trash.")
print("Failed to move application to trash.\n")
exit(1)
}
}
@@ -115,7 +136,7 @@ func processCLI(arguments: [String], appState: AppState, locations: Locations) {
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")
print("[BETA] Pearcleaner CLI | Uninstall Application & Related Files:\n")
// Fetch the app info and safely unwrap
guard let appInfo = AppInfoFetcher.getAppInfo(atPath: url) else {
@@ -132,46 +153,75 @@ func processCLI(arguments: [String], appState: AppState, locations: Locations) {
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)
}
print("The application and related files have been moved to the trash successfully.\n")
exit(0)
} else {
print("Failed to move application and related files to trash.")
print("Failed to move application and related files to trash.\n")
exit(1)
}
}
}
// Private function to remove the leftover files
func removeLeftoverFiles() {
print("[BETA] Pearcleaner CLI | Remove Leftover Files:\n")
// Get installed apps for filtering
let sortedApps = getSortedApps(paths: fsm.folderPaths)
// Find leftover files
let foundPaths = ReversePathsSearcher(locations: locations, fsm: fsm, sortedApps: sortedApps)
.reversePathsSearchCLI()
let success = moveFilesToTrashCLI(at: foundPaths)
if success {
print("Leftover files have been moved to the trash successfully.\n")
exit(0)
} else {
print("Failed to move leftover files to trash.\n")
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
// Handle --list or -l option with a path argument for listing app bundle files
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
// Handle --listlf or -lf option with a path argument for listing leftover files
if options.contains("--list-leftover") || options.contains("-lf") {
listLeftoverFiles()
exit(0)
}
// Handle --uninstall or -u option with a path argument to uninstall app bundle only
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
// Handle --uninstall-all or -ua option with a path argument to uninstall app bundle and related files
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)
}
// Handle --uninstall-lf or -ulf option with a path argument for listing leftover files
if options.contains("--remove-leftover") || options.contains("-rl") {
removeLeftoverFiles()
exit(0)
}
// If no valid option was provided, show the help menu by default
displayHelp()
exit(0)
@@ -183,9 +233,11 @@ func displayHelp() {
[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
--list <path>, -l <path> List application files available for uninstall at the specified path
--list-leftover, -lf List leftover files available for removal
--uninstall <path>, -u <path> Uninstall only the application bundle at the specified path
--uninstall-all <path>, -ua <path> Uninstall application bundle and ALL related files at the specified path
--remove-leftover, -rl Remove ALL leftover files (To ignore files, add them to the exception list within Pearcleaner settings)
--help, -h Show this help message
""")
@@ -197,8 +249,16 @@ 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)
// Check if the file exists and is a symlink
guard fileManager.fileExists(atPath: filePath) else { return false }
// Check if the symlink points to the correct path
do {
let destination = try fileManager.destinationOfSymbolicLink(atPath: filePath)
return destination == Bundle.main.executablePath
} catch {
return false
}
}
// Install/uninstall symlink for CLI
@@ -278,6 +338,93 @@ func manageFinderPlugin(install: Bool) {
// Brew cleanup
func caskCleanup(app: String) {
Task(priority: .high) {
#if arch(x86_64)
let cmd = "/usr/local/bin/brew"
#elseif arch(arm64)
let cmd = "/opt/homebrew/bin/brew"
#endif
if let formattedApp = getCaskIdentifier(for: app) {
// App found, proceed with cleanup
let script = """
tell application "Terminal"
activate
do script "/bin/bash --noprofile --norc -c '
clear;
echo \\"[Pearcleaner] Homebrew cleanup for \(app):\n\\";
\(cmd) uninstall --cask \(formattedApp) --force;
\(cmd) cleanup;
killall Terminal;
'" in front window
end tell
"""
var error: NSDictionary?
if let appleScript = NSAppleScript(source: script) {
appleScript.executeAndReturnError(&error)
}
if let error = error {
printOS("AppleScript Error: \(error)")
}
} else {
printOS("Brew cleanup: No cask found for \(app).")
}
}
}
func getCaskIdentifier(for appName: String) -> String? {
#if arch(x86_64)
let caskroomPath = "/usr/local/Caskroom/"
#elseif arch(arm64)
let caskroomPath = "/opt/homebrew/Caskroom/"
#endif
let fileManager = FileManager.default
let lowercasedAppName = appName.lowercased()
do {
// Get all cask directories from Caskroom, ignoring hidden files
let casks = try fileManager.contentsOfDirectory(atPath: caskroomPath).filter { !$0.hasPrefix(".") }
for cask in casks {
// Construct the path to the cask directory
let caskSubPath = caskroomPath + cask
// Get all version directories for this cask, ignoring hidden files
let versions = try fileManager.contentsOfDirectory(atPath: caskSubPath).filter { !$0.hasPrefix(".") }
// Only check the first valid version directory to improve efficiency
if let latestVersion = versions.first {
let appDirectory = "\(caskSubPath)/\(latestVersion)/"
// List all files in the version directory and check for .app file
let appsInDir = try fileManager.contentsOfDirectory(atPath: appDirectory).filter { !$0.hasPrefix(".") }
if let appFile = appsInDir.first(where: { $0.hasSuffix(".app") }) {
let realAppName = appFile.replacingOccurrences(of: ".app", with: "").lowercased()
// Compare the lowercased app names for case-insensitive match
if realAppName == lowercasedAppName {
return realAppName.replacingOccurrences(of: " ", with: "-").lowercased()
}
}
}
}
} catch {
print("Error reading cask metadata: \(error)")
}
// If no match is found, return nil
return nil
}
func caskCleanup2(app: String) {
Task(priority: .high) {
let formattedApp = app.lowercased().replacingOccurrences(of: " ", with: "-")
#if arch(x86_64)
@@ -289,20 +436,23 @@ func caskCleanup(app: String) {
let script = """
tell application "Terminal"
activate
do script "
do script "/bin/bash --noprofile --norc -c '
found_cask=$(\(cmd) list --cask | grep '\(formattedApp)');
if [ -n \\"$found_cask\\" ]; then
clear;
echo \\"[Pearcleaner] Homebrew cleanup for \(formattedApp):\n\\";
\(cmd) uninstall --cask \\"$found_cask\\" --force;
\(cmd) cleanup;
killall Terminal;
else
echo \\"Cask not found for \(formattedApp)\\";
fi"
clear;
echo \\"[Pearcleaner] \(formattedApp) cask not found for homebrew cleanup!\nClosing window in 5 seconds..\\";
sleep 5;
killall Terminal;
fi'" in front window
end tell
"""
print(script)
var error: NSDictionary?
if let appleScript = NSAppleScript(source: script) {
appleScript.executeAndReturnError(&error)
@@ -315,7 +465,6 @@ func caskCleanup(app: String) {
}
// Print list of files locally
func saveURLsToFile(urls: Set<URL>, appState: AppState, copy: Bool = false) {
@@ -447,10 +596,7 @@ func removeApp(appState: AppState, withPath path: URL) {
appState.sortedApps.remove(at: index)
// return // Exit the function if the app was found and removed
}
// Remove from appInfoStore if found
// if let index = appState.appInfoStore.firstIndex(where: { $0.path == path }) {
// appState.appInfoStore.remove(at: index)
// }
// Brew cleanup if enabled
if brew {
caskCleanup(app: appState.appInfo.appName)
@@ -481,26 +627,26 @@ func showLocalized(url: URL) -> String {
}
extension URL {
func localizedName() -> String? {
func localizedName() -> String {
do {
let resourceValues = try self.resourceValues(forKeys: [.localizedNameKey])
return resourceValues.localizedName
return resourceValues.localizedName?.replacingOccurrences(of: ".app", with: "") ?? self.lastPathComponent.replacingOccurrences(of: ".app", with: "")
} catch {
print("Error getting localized name: \(error)")
return nil
return self.lastPathComponent.replacingOccurrences(of: ".app", with: "")
}
}
}
extension String {
func localizedName() -> String? {
func localizedName() -> String {
let url = URL(fileURLWithPath: self)
do {
let resourceValues = try url.resourceValues(forKeys: [.localizedNameKey])
return resourceValues.localizedName
return resourceValues.localizedName?.replacingOccurrences(of: ".app", with: "") ?? self
} catch {
print("Error getting localized name: \(error)")
return nil
return self
}
}
}
+2 -2
View File
@@ -43,7 +43,7 @@ struct PearcleanerApp: App {
// 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)
processCLI(arguments: arguments, appState: appState, locations: locations, fsm: fsm)
}
}
@@ -128,7 +128,7 @@ struct PearcleanerApp: App {
.preferredColorScheme(themeManager.displayMode.colorScheme)
})
}
#if !DEBUG
Task {
+6 -5
View File
@@ -359,19 +359,20 @@ struct FilesView: View {
}
} else {
// Run brew cleanup if sent from Sentinel
if brew && appState.sentinelMode {
caskCleanup(app: appState.appInfo.appName)
}
// Add deleted appInfo object to trashed array
appState.trashedFiles.append(appState.appInfo)
// Clear out appInfoStore object (Used for leftover file search)
// if let index = appState.appInfoStore.firstIndex(where: { $0.path == appState.appInfo.path }) {
// appState.appInfoStore[index] = .empty
// }
updateOnMain {
// Remove items from the list
appState.appInfo.fileSize = appState.appInfo.fileSize.filter { !appState.selectedItems.contains($0.key) }
// Update the selectedFiles to remove references that are no longer present
appState.selectedItems.removeAll()
appState.sentinelMode = false
if appState.oneShotMode {
NSApp.terminate(nil)
}
+1 -1
View File
@@ -56,7 +56,7 @@ func checkApp(file: String) {
return
} else {
if FileManager.default.isInTrash(app) {
NSWorkspace.shared.open(URL(string: "pear://com.alienator88.Pearcleaner?path=\(file)")!)
NSWorkspace.shared.open(URL(string: "pear://com.alienator88.Pearcleaner?path=\(file)&brew=true")!)
}
}
} else {