diff --git a/Pearcleaner.xcodeproj/project.pbxproj b/Pearcleaner.xcodeproj/project.pbxproj index 83b995b..52eadf5 100644 --- a/Pearcleaner.xcodeproj/project.pbxproj +++ b/Pearcleaner.xcodeproj/project.pbxproj @@ -12,6 +12,7 @@ C7045A322B068AD700376976 /* NewWin.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7045A312B068AD700376976 /* NewWin.swift */; }; C7045A342B068B2900376976 /* UpdateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7045A332B068B2900376976 /* UpdateView.swift */; }; C7045A362B068DD700376976 /* PermView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7045A352B068DD700376976 /* PermView.swift */; }; + C71848442B8D2D600046CB13 /* ZombieView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C71848432B8D2D600046CB13 /* ZombieView.swift */; }; C72893052AFD42E600C8C1CD /* DeepLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = C72893042AFD42E600C8C1CD /* DeepLink.swift */; }; C72893122AFD51EA00C8C1CD /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = C72893112AFD51EA00C8C1CD /* main.swift */; }; C7575D382B0181FC006A600A /* FileWatcher in Frameworks */ = {isa = PBXBuildFile; productRef = C7575D372B0181FC006A600A /* FileWatcher */; }; @@ -30,6 +31,7 @@ C77B901E2AF1A9DB009CC655 /* SettingsWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C77B901D2AF1A9DB009CC655 /* SettingsWindow.swift */; }; C77B90232AF2D616009CC655 /* FilesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C77B90222AF2D616009CC655 /* FilesView.swift */; }; C7A27E812AFD7C4600166168 /* com.alienator88.PearcleanerSentinel.plist in Resources */ = {isa = PBXBuildFile; fileRef = C7A27E802AFD7C4600166168 /* com.alienator88.PearcleanerSentinel.plist */; }; + C7A9CE472B89164700EB6E78 /* Authorization.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7A9CE462B89164700EB6E78 /* Authorization.swift */; }; C7CF47242B3B3F1700979C5F /* WindowSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7CF47232B3B3F1700979C5F /* WindowSettings.swift */; }; C7D31D462AFDA90C00C7ED9E /* Sentinel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7D31D452AFDA90C00C7ED9E /* Sentinel.swift */; }; C7D31D482AFEB23700C7ED9E /* TopBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7D31D472AFEB23700C7ED9E /* TopBar.swift */; }; @@ -68,6 +70,7 @@ C7045A312B068AD700376976 /* NewWin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewWin.swift; sourceTree = ""; }; C7045A332B068B2900376976 /* UpdateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateView.swift; sourceTree = ""; }; C7045A352B068DD700376976 /* PermView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermView.swift; sourceTree = ""; }; + C71848432B8D2D600046CB13 /* ZombieView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZombieView.swift; sourceTree = ""; }; C72893042AFD42E600C8C1CD /* DeepLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeepLink.swift; sourceTree = ""; }; 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 = ""; }; @@ -89,6 +92,7 @@ C77B90242AF2D796009CC655 /* Pearcleaner.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Pearcleaner.entitlements; sourceTree = ""; }; C7A27E7F2AFD70DD00166168 /* PearcleanerSentinel.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = PearcleanerSentinel.entitlements; sourceTree = ""; }; C7A27E802AFD7C4600166168 /* com.alienator88.PearcleanerSentinel.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = com.alienator88.PearcleanerSentinel.plist; sourceTree = ""; }; + C7A9CE462B89164700EB6E78 /* Authorization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Authorization.swift; sourceTree = ""; }; C7CF47232B3B3F1700979C5F /* WindowSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowSettings.swift; sourceTree = ""; }; C7D31D452AFDA90C00C7ED9E /* Sentinel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sentinel.swift; sourceTree = ""; }; C7D31D472AFEB23700C7ED9E /* TopBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopBar.swift; sourceTree = ""; }; @@ -140,6 +144,7 @@ C7D31D502AFF00F300C7ED9E /* Locations.swift */, C7D31D542B008CB900C7ED9E /* Shapes.swift */, C7CF47232B3B3F1700979C5F /* WindowSettings.swift */, + C7A9CE462B89164700EB6E78 /* Authorization.swift */, ); path = Logic; sourceTree = ""; @@ -227,6 +232,7 @@ C7D31D492AFEB26700C7ED9E /* AppListItems.swift */, C7D31D582B00A49300C7ED9E /* DropTarget.swift */, C7045A272B03E71D00376976 /* MiniMode.swift */, + C71848432B8D2D600046CB13 /* ZombieView.swift */, ); path = Views; sourceTree = ""; @@ -358,6 +364,7 @@ C77B901C2AF193A3009CC655 /* Styles.swift in Sources */, C7045A282B03E71D00376976 /* MiniMode.swift in Sources */, C72893052AFD42E600C8C1CD /* DeepLink.swift in Sources */, + C71848442B8D2D600046CB13 /* ZombieView.swift in Sources */, C76D084C2AF83C9D00D07867 /* Permissions.swift in Sources */, C76D084A2AF83C6E00D07867 /* General.swift in Sources */, C77B90042AF18E2E009CC655 /* PearcleanerApp.swift in Sources */, @@ -370,6 +377,7 @@ C7D31D482AFEB23700C7ED9E /* TopBar.swift in Sources */, C7F539382AF60865007DF1B2 /* Utilities.swift in Sources */, C76D08552AF89CDE00D07867 /* AppListView.swift in Sources */, + C7A9CE472B89164700EB6E78 /* Authorization.swift in Sources */, C7CF47242B3B3F1700979C5F /* WindowSettings.swift in Sources */, C77B90232AF2D616009CC655 /* FilesView.swift in Sources */, ); diff --git a/Pearcleaner/Logic/AppCommands.swift b/Pearcleaner/Logic/AppCommands.swift index 4c1a9b7..106bfac 100644 --- a/Pearcleaner/Logic/AppCommands.swift +++ b/Pearcleaner/Logic/AppCommands.swift @@ -34,11 +34,24 @@ struct AppCommands: Commands { // Refresh Apps list appState.reload.toggle() let sortedApps = getSortedApps() - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + updateOnMain { + appState.sortedApps.userApps = [] + appState.sortedApps.systemApps = [] appState.sortedApps.userApps = sortedApps.userApps appState.sortedApps.systemApps = sortedApps.systemApps + } + Task(priority: .high){ + loadAllPaths(allApps: sortedApps.userApps + sortedApps.systemApps, appState: appState, locations: locations) + } + updateOnMain { appState.reload.toggle() } +// let sortedApps = getSortedApps() +// DispatchQueue.main.asyncAfter(deadline: .now() + 1) { +// appState.sortedApps.userApps = sortedApps.userApps +// appState.sortedApps.systemApps = sortedApps.systemApps +// appState.reload.toggle() +// } } } label: { Text("Refresh Apps") @@ -50,10 +63,6 @@ struct AppCommands: Commands { } label: { Text("Uninstall Pearcleaner") } -// .keyboardShortcut("C", modifiers: .command) - - - } @@ -65,10 +74,13 @@ struct AppCommands: Commands { { undoTrash(appState: appState) { let sortedApps = getSortedApps() - appState.sortedApps.userApps = [] - appState.sortedApps.systemApps = [] - appState.sortedApps.userApps = sortedApps.userApps - appState.sortedApps.systemApps = sortedApps.systemApps + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + appState.sortedApps.userApps = [] + appState.sortedApps.systemApps = [] + appState.sortedApps.userApps = sortedApps.userApps + appState.sortedApps.systemApps = sortedApps.systemApps + loadAllPaths(allApps: sortedApps.userApps + sortedApps.systemApps, appState: appState, locations: locations) + } } } label: { Label("Undo Removal", systemImage: "clear") diff --git a/Pearcleaner/Logic/AppState.swift b/Pearcleaner/Logic/AppState.swift index 6edb0e3..9cc8e08 100644 --- a/Pearcleaner/Logic/AppState.swift +++ b/Pearcleaner/Logic/AppState.swift @@ -13,9 +13,12 @@ let home = FileManager.default.homeDirectoryForCurrentUser.path class AppState: ObservableObject { @Published var appInfo: AppInfo - @Published var paths: [URL] = [] + @Published var appInfoStore: [AppInfo] = [] + @Published var zombieFile: ZombieFile @Published var sortedApps: (userApps: [AppInfo], systemApps: [AppInfo]) = ([], []) @Published var selectedItems = Set() + @Published var selectedZombieItems = Set() + @Published var trashedFiles: [URL] = [] @Published var alertType = AlertType.off @Published var currentView = CurrentDetailsView.empty @Published var showAlert: Bool = false @@ -23,12 +26,11 @@ class AppState: ObservableObject @Published var isReminderVisible: Bool = false @Published var releases = [Release]() @Published var progressBar: (String, Double) = ("Ready", 0.0) -// @Published var progressManager = ProgressManager() @Published var reload: Bool = false + @Published var showProgress: Bool = false + @Published var popCount: Int = 0 + - - //Window -// @Published var winWidth: CGFloat = 1020 init() { self.appInfo = AppInfo( @@ -39,45 +41,27 @@ class AppState: ObservableObject appVersion: "", appIcon: nil, webApp: false, - wrapped: false + wrapped: false, + files: [], + fileSize: [:], + fileIcon: [:] + ) + + self.zombieFile = ZombieFile( + id: UUID(), +// files: [], + fileSize: [:], + fileIcon: [:] ) } } -//class ProgressManager: ObservableObject { -// @Published var progress: Double = 0.0 -// @Published var total: Double = 0.0 -// @Published var status: String = "Ready" -// -// func setTotal(_ total: Double) { -// DispatchQueue.main.async { -// self.total = total -// } -// } -// -// func updateProgress() { -// DispatchQueue.main.async { -// self.progress = min(max(0.0, self.progress + 1.0), Double(self.total)) -// } -// } -// -// func updateStatus(status: String) { -// DispatchQueue.main.async { -// self.status = status -// } -// } -// -// func resetProgress() { -// DispatchQueue.main.async { -// self.progress = 0.0 -// } -// } -//} -struct AppInfo: Identifiable, Hashable { + +struct AppInfo: Identifiable, Equatable, Hashable { let id: UUID let path: URL let bundleIdentifier: String @@ -86,11 +70,40 @@ struct AppInfo: Identifiable, Hashable { let appIcon: NSImage? let webApp: Bool let wrapped: Bool + var files: [URL] + var fileSize: [URL:Int64] + var fileIcon: [URL:NSImage?] + var totalSize: Int64 + { + return fileSize.values.reduce(0, +) + } + + + static let empty = AppInfo(id: UUID(), path: URL(fileURLWithPath: ""), bundleIdentifier: "", appName: "", appVersion: "", appIcon: nil, webApp: false, wrapped: false, files: [], fileSize: [:], fileIcon: [:]) - static let empty = AppInfo(id: UUID(), path: URL(fileURLWithPath: ""), bundleIdentifier: "", appName: "", appVersion: "", appIcon: nil, webApp: false, wrapped: false) } + +struct ZombieFile: Identifiable, Equatable, Hashable { + let id: UUID +// var files: [URL] + var fileSize: [URL:Int64] + var fileIcon: [URL:NSImage?] + var totalSize: Int64 + { + return fileSize.values.reduce(0, +) + } + + + static let empty = ZombieFile(id: UUID(), fileSize: [:], fileIcon: [:]) + +} + + + + + enum CurrentTabView:Int { case general @@ -115,6 +128,7 @@ enum CurrentDetailsView:Int case empty case files case apps + case zombie } enum NewWindow:Int @@ -171,6 +185,36 @@ enum DisplayMode: Int, CaseIterable { } +//class ProgressManager: ObservableObject { +// @Published var progress: Double = 0.0 +// @Published var total: Double = 0.0 +// @Published var status: String = "Ready" +// +// func setTotal(_ total: Double) { +// DispatchQueue.main.async { +// self.total = total +// } +// } +// +// func updateProgress() { +// DispatchQueue.main.async { +// self.progress = min(max(0.0, self.progress + 1.0), Double(self.total)) +// } +// } +// +// func updateStatus(status: String) { +// DispatchQueue.main.async { +// self.status = status +// } +// } +// +// func resetProgress() { +// DispatchQueue.main.async { +// self.progress = 0.0 +// } +// } +//} + //let (cacheDir, tempDir) = darwinCT() //let locations: [String] = [ diff --git a/Pearcleaner/Logic/Authorization.swift b/Pearcleaner/Logic/Authorization.swift new file mode 100644 index 0000000..a87a6c9 --- /dev/null +++ b/Pearcleaner/Logic/Authorization.swift @@ -0,0 +1,113 @@ +// +// Authorization.swift +// Pearcleaner +// +// Created by Alin Lupascu on 2/23/24. +// + +import Foundation +import Swift + +// https://github.com/x13a/authorization-swift + +public struct Authorization { + + public enum Error: Swift.Error { + case create(OSStatus) + case copyRights(OSStatus) + case exec(OSStatus) + } + + public static func executeWithPrivileges( + _ command: String + ) -> Result { + + let RTLD_DEFAULT = UnsafeMutableRawPointer(bitPattern: -2) + var fn: @convention(c) ( + AuthorizationRef, + UnsafePointer, // path + AuthorizationFlags, + UnsafePointer?>, // args + UnsafeMutablePointer>? + ) -> OSStatus + fn = unsafeBitCast( + dlsym(RTLD_DEFAULT, "AuthorizationExecuteWithPrivileges"), + to: type(of: fn) + ) + + var authorizationRef: AuthorizationRef? = nil + var err = AuthorizationCreate(nil, nil, [], &authorizationRef) + guard err == errAuthorizationSuccess else { + return .failure(.create(err)) + } + defer { AuthorizationFree(authorizationRef!, [.destroyRights]) } + + var components = command.components(separatedBy: " ") + var path = components.remove(at: 0).cString(using: .utf8)! + let name = kAuthorizationRightExecute.cString(using: .utf8)! + + var items: AuthorizationItem = name.withUnsafeBufferPointer { nameBuf in + path.withUnsafeBufferPointer { pathBuf in + let pathPtr = + UnsafeMutableRawPointer(mutating: pathBuf.baseAddress!) + return AuthorizationItem( + name: nameBuf.baseAddress!, + valueLength: path.count, + value: pathPtr, + flags: 0 + ) + } + } + + var rights: AuthorizationRights = + withUnsafeMutablePointer(to: &items) { items in + return AuthorizationRights(count: 1, items: items) + } + + let flags: AuthorizationFlags = [ + .interactionAllowed, + .preAuthorize, + .extendRights, + ] + + err = AuthorizationCopyRights( + authorizationRef!, + &rights, + nil, + flags, + nil + ) + guard err == errAuthorizationSuccess else { + return .failure(.copyRights(err)) + } + + let rest = components.map { $0.cString(using: .utf8)! } + var args = Array?>( + repeating: nil, + count: rest.count + 1 + ) + for (idx, arg) in rest.enumerated() { + args[idx] = UnsafePointer?(arg) + } + + var file = FILE() + let fh: FileHandle? + + (err, fh) = withUnsafeMutablePointer(to: &file) { file in + var pipe = file + let err = fn(authorizationRef!, &path, [], &args, &pipe) + guard err == errAuthorizationSuccess else { + return (err, nil) + } + let fh = FileHandle( + fileDescriptor: fileno(pipe), + closeOnDealloc: true + ) + return (err, fh) + } + guard err == errAuthorizationSuccess else { + return .failure(.exec(err)) + } + return .success(fh!) + } +} diff --git a/Pearcleaner/Logic/DeepLink.swift b/Pearcleaner/Logic/DeepLink.swift index cbade36..b78024d 100644 --- a/Pearcleaner/Logic/DeepLink.swift +++ b/Pearcleaner/Logic/DeepLink.swift @@ -23,8 +23,10 @@ class DeeplinkManager { } func manage(url: URL, appState: AppState, locations: Locations) { + // This handles dropping an app onto Pearcleaner if url.pathExtension == "app" { handleAppBundle(url: url, appState: appState, locations: locations) + // This handles sentinel monitor launch } else if url.scheme == DeepLinkConstants.scheme, url.host == DeepLinkConstants.host, let components = URLComponents(url: url, resolvingAgainstBaseURL: true), @@ -32,57 +34,88 @@ class DeeplinkManager { if let path = queryItems.first(where: { $0.name == DeepLinkConstants.query })?.value { let pathURL = URL(fileURLWithPath: path) let appInfo = getAppInfo(atPath: pathURL) - updateOnMain { - appState.appInfo = appInfo! - findPathsForApp(appState: appState, locations: locations) - if self.mini { - self.showPopover = true - } else { - appState.currentView = .files - } - } + showAppInFiles(appInfo: appInfo!, mini: mini, appState: appState, locations: locations, showPopover: $showPopover) + +// showPopover = false +// updateOnMain { +// appState.appInfo = .empty +// if let storedAppInfo = appState.appInfoStore.first(where: { $0.path == appInfo?.path }) { +// appState.appInfo = storedAppInfo +//// appState.paths = storedAppInfo.fileSize.keys.map { $0 }//storedAppInfo.files +// appState.selectedItems = Set(storedAppInfo.files) +// withAnimation(Animation.easeIn(duration: 0.4)) { +// if self.mini { +// self.showPopover.toggle() +// } else { +// appState.currentView = .files +// } +// } +// } else { +// // Handle the case where the appInfo is not found in the store +// printOS("AppInfo not found in the cached store, searching again") +// appState.appInfo = .empty +// appState.appInfo = appInfo! +// findPathsForApp(appInfo: appInfo!, appState: appState, locations: locations) +// withAnimation(Animation.easeIn(duration: 0.4)) { +// if self.mini { +// self.showPopover.toggle() +// } else { +// appState.currentView = .files +// } +// } +// } +// } } else { - print("No path query parameter found in the URL") + printOS("No path query parameter found in the URL") } } else { - print("URL does not match the expected scheme and host") + printOS("URL does not match the expected scheme and host") } } - -// func manage(url: URL, appState: AppState) { -// guard url.scheme == DeepLinkConstants.scheme, -// url.host == DeepLinkConstants.host, -// let components = URLComponents(url: url, resolvingAgainstBaseURL: true), -// let queryItems = components.queryItems -// else { -// print("URL does not match the expected scheme and host") -// return -// } -// -// if let path = queryItems.first(where: { $0.name == DeepLinkConstants.query })?.value { -// let pathURL = URL(fileURLWithPath: path) -// let appInfo = getAppInfo(atPath: pathURL) -// updateOnMain { -// appState.appInfo = appInfo! -// findPathsForApp(appState: appState, appInfo: appState.appInfo) -// appState.currentView = .files -// } -// } else { -// print("No path query parameter found in the URL") -// } -// } + func handleAppBundle(url: URL, appState: AppState, locations: Locations) { let appInfo = getAppInfo(atPath: url) - updateOnMain { - appState.appInfo = appInfo! - findPathsForApp(appState: appState, locations: locations) - if self.mini { - self.showPopover = true - } else { - appState.currentView = .files - } - } + showAppInFiles(appInfo: appInfo!, mini: mini, appState: appState, locations: locations, showPopover: $showPopover) + +// showPopover = false +// updateOnMain { +// appState.appInfo = .empty +// if let storedAppInfo = appState.appInfoStore.first(where: { $0.path == appInfo?.path }) { +// appState.appInfo = storedAppInfo +//// appState.paths = storedAppInfo.fileSize.keys.map { $0 }//storedAppInfo.files +// appState.selectedItems = Set(storedAppInfo.files) +// withAnimation(Animation.easeIn(duration: 0.4)) { +// if self.mini { +// self.showPopover.toggle() +// } else { +// appState.currentView = .files +// } +// } +// } else { +// // Handle the case where the appInfo is not found in the store +// printOS("AppInfo not found in the cached store, searching again") +// appState.appInfo = .empty +// appState.appInfo = appInfo! +// findPathsForApp(appInfo: appInfo!, appState: appState, locations: locations) +// withAnimation(Animation.easeIn(duration: 0.4)) { +// if self.mini { +// self.showPopover.toggle() +// } else { +// appState.currentView = .files +// } +// } +// } +// } +// updateOnMain { +// appState.appInfo = appInfo! +// findPathsForApp(appState: appState, locations: locations) +// if self.mini { +// self.showPopover = true +// } else { +// appState.currentView = .files +// } +// } } } diff --git a/Pearcleaner/Logic/Locations.swift b/Pearcleaner/Logic/Locations.swift index e8e16a3..9528191 100644 --- a/Pearcleaner/Logic/Locations.swift +++ b/Pearcleaner/Logic/Locations.swift @@ -19,6 +19,7 @@ class Locations: ObservableObject { let tempDir: String var apps: Category + var reverse: Category // var widgets: Category // var plugins: Category @@ -32,10 +33,8 @@ class Locations: ObservableObject { "\(home)/Library", "\(home)/Library/Application Scripts", "\(home)/Library/Application Support", -// "\(home)/Library/Application Support/CrashReporter", "\(home)/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.ApplicationRecentDocuments", "\(home)/Library/Containers", -// "\(home)/Library/Group Containers", // This is now handled by the function getGroupContainers() "\(home)/Library/Caches", "\(home)/Library/HTTPStorages", "\(home)/Library/Internet Plug-Ins", @@ -75,9 +74,36 @@ class Locations: ObservableObject { let subfolders = listAppSupportDirectories() for folder in subfolders { self.apps.paths.append("\(home)/Library/Application Support/\(folder)") -// writeLog(string: "Adding subfolder: \(home)/Library/Application Support/\(folder)") } + + + + self.reverse = Category(name: "Reverse", paths: [ + "\(home)/Library/Application Scripts", + "\(home)/Library/Application Support", + "\(home)/Library/Application Support/Caches", + "\(home)/Library/Containers", + "\(home)/Library/Caches", + "\(home)/Library/HTTPStorages", + "\(home)/Library/Internet Plug-Ins", + "\(home)/Library/LaunchAgents", + "\(home)/Library/Logs", + "\(home)/Library/Preferences", + "\(home)/Library/Preferences/ByHost", + "\(home)/Library/Saved Application State", + "\(home)/Library/WebKit", + "/Library/Application Support", + "/Library/Application Support/CrashReporter", + "/Library/Internet Plug-Ins", + "/Library/LaunchAgents", + "/Library/LaunchDaemons", + "/Library/PrivilegedHelperTools", + "/private/var/db/receipts", + cacheDir, + tempDir + ]) + // self.widgets = Category(name: "Widgets", paths: [ // // User // "~/Library/Widgets", diff --git a/Pearcleaner/Logic/Logic.swift b/Pearcleaner/Logic/Logic.swift index c739ed3..8ce9617 100644 --- a/Pearcleaner/Logic/Logic.swift +++ b/Pearcleaner/Logic/Logic.swift @@ -7,6 +7,7 @@ import Foundation import SwiftUI +//import AudioToolbox // Get list of apps and sort it @@ -53,7 +54,7 @@ func getApplications() -> (systemApps: [URL], userApps: [URL]) { } } catch { // Handle any potential errors here - print("Error: \(error)") + printOS("Error: \(error)") } } @@ -86,7 +87,7 @@ func getAppInfo(atPath path: URL) -> AppInfo? { if let bundleVersion = bundle.infoDictionary?["CFBundleVersion"] as? String { appVersion = bundleVersion } else { - print("Failed to retrieve bundle version") + printOS("Failed to retrieve bundle version") } } @@ -123,7 +124,7 @@ func getAppInfo(atPath path: URL) -> AppInfo? { } if appIcon == nil { - print("App Icon not found for app at path: \(path)") + printOS("App Icon not found for app at path: \(path)") } if bundle.infoDictionary?["LSTemplateApplication"] is Bool { @@ -133,7 +134,7 @@ func getAppInfo(atPath path: URL) -> AppInfo? { } - return AppInfo(id: UUID(), path: path, bundleIdentifier: bundleIdentifier, appName: appName ?? "", appVersion: appVersion, appIcon: appIcon, webApp: webApp ?? false, wrapped: false) + return AppInfo(id: UUID(), path: path, bundleIdentifier: bundleIdentifier, appName: appName ?? "", appVersion: appVersion, appIcon: appIcon, webApp: webApp ?? false, wrapped: false, files: [], fileSize: [:], fileIcon: [:]) } else { let wrapperURL = path.appendingPathComponent("Wrapper") @@ -150,18 +151,18 @@ func getAppInfo(atPath path: URL) -> AppInfo? { return wrappedAppInfo } } else { - print("No .app files found in the 'Wrapper' directory.") + printOS("No .app files found in the 'Wrapper' directory.") } } catch { - print("Error reading contents of 'Wrapper' directory: \(error.localizedDescription)") + printOS("Error reading contents of 'Wrapper' directory: \(error.localizedDescription)") } } else { - print("Error: 'Wrapper' directory not found at path: \(wrapperURL.path)") + printOS("Error: 'Wrapper' directory not found at path: \(wrapperURL.path)") } } } else { - print("Bundle not found at path: \(path)") + printOS("Bundle not found at path: \(path)") } return nil } @@ -201,10 +202,10 @@ func getWrappedAppInfo(atPath path: URL) -> AppInfo? { } else { - print("No matching image found for \(primaryIconFile).") + printOS("No matching image found for \(primaryIconFile).") } } else { - print("Unable to access the directory at \(primaryIconFile).") + printOS("Unable to access the directory at \(primaryIconFile).") } } @@ -217,7 +218,7 @@ func getWrappedAppInfo(atPath path: URL) -> AppInfo? { } if appIcon == nil { - print("App Icon not found for app at path: \(path)") + printOS("App Icon not found for app at path: \(path)") } if bundle.infoDictionary?["LSTemplateApplication"] is Bool { @@ -227,13 +228,13 @@ func getWrappedAppInfo(atPath path: URL) -> AppInfo? { } - return AppInfo(id: UUID(), path: path, bundleIdentifier: bundleIdentifier, appName: appName ?? "", appVersion: appVersion, appIcon: appIcon, webApp: webApp ?? false, wrapped: true) + return AppInfo(id: UUID(), path: path, bundleIdentifier: bundleIdentifier, appName: appName ?? "", appVersion: appVersion, appIcon: appIcon, webApp: webApp ?? false, wrapped: true, files: [], fileSize: [:], fileIcon: [:]) } else { - print("One or more variables missing for app at path: \(path)") + printOS("One or more variables missing for app at path: \(path)") } } else { - print("Bundle not found at path: \(path)") + printOS("Bundle not found at path: \(path)") } return nil } @@ -263,28 +264,11 @@ func darwinCT() -> (String, String) { return (cacheDir, tempDir) } } - print("Could not get DARWIN_USER_CACHE_DIR or DARWIN_USER_TEMP_DIR") + printOS("Could not get DARWIN_USER_CACHE_DIR or DARWIN_USER_TEMP_DIR") return ("", "") } -// Add subfolders of ~/Library/Application Support/ to locations for deeper search -//func appSupSubfolders2() throws -> [String] { -// let fileManager = FileManager.default -// let appSup = "\(home)/Library/Application Support/" -// let subfolders = try fileManager.contentsOfDirectory(atPath: appSup) -// let exclusionRegex = try NSRegularExpression(pattern: "\\bcom\\.apple\\b", options: []) -// let exclusions = ["MobileSync", ".DS_Store", "Xcode", "SyncServices", "networkserviceproxy", "DiskImages", "CallHistoryTransactions", "App Store", "CloudDocs", "icdd", "iCloud", "Instruments", "AddressBook", "FaceTime", "AskPermission", "CallHistoryDB"] -// -// let allowedFolders = subfolders.filter { folder in -// let range = NSRange(location: 0, length: folder.utf16.count) -// return exclusionRegex.firstMatch(in: folder, options: [], range: range) == nil && !exclusions.contains(folder) -// } -// -// return allowedFolders -//} - - func listAppSupportDirectories() -> [String] { let home = FileManager.default.homeDirectoryForCurrentUser let appSupportLocation = home.appendingPathComponent("Library/Application Support/") @@ -312,7 +296,7 @@ func listAppSupportDirectories() -> [String] { return filteredDirectories } catch { - print("Error listing AppSupport directories: \(error.localizedDescription)") + printOS("Error listing AppSupport directories: \(error.localizedDescription)") return [] } } @@ -334,19 +318,16 @@ func killApp(appId: String, completion: @escaping () -> Void = {}) { // Find all possible paths for an app based on name/bundle id -func findPathsForApp(appState: AppState, locations: Locations) { +func findPathsForApp(appInfo: AppInfo = .empty, appState: AppState, locations: Locations, backgroundRun: Bool = false, completion: @escaping () -> Void = {}) { Task(priority: .high) { - updateOnMain { - appState.paths = [] - } - let appInfo = appState.appInfo + var collection: [URL] = [] if let url = URL(string: appInfo.path.absoluteString) { collection.insert(url, at: 0) } - + + let fileManager = FileManager.default -// let progressManager = appState.progressManager let dispatchGroup = DispatchGroup() var bundleComponents = appInfo.bundleIdentifier.components(separatedBy: ".") if let lastComponent = bundleComponents.last, let rangeOfDash = lastComponent.range(of: "-") { @@ -368,16 +349,6 @@ func findPathsForApp(appState: AppState, locations: Locations) { continue } - -// DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { -// withAnimation { -// progressManager.updateStatus(status: location) -// progressManager.updateProgress() -// progressManager.objectWillChange.send() -// } -// } - - dispatchGroup.enter() // Enter the dispatch group @@ -385,12 +356,19 @@ func findPathsForApp(appState: AppState, locations: Locations) { do { let contents = try fileManager.contentsOfDirectory(atPath: location) - + for item in contents { + let itemURL = URL(fileURLWithPath: location).appendingPathComponent(item) let itemL = ("\(item)").replacingOccurrences(of: ".", with: "").replacingOccurrences(of: " ", with: "").lowercased() + let filterItem = "\(location)/\(itemL)" + // Skip directories that start with com.apple. except for a few + if itemL.hasPrefix("comapple") && !itemL.hasPrefix("comappleconfigurator") && !itemL.hasPrefix("comappledt") && !itemL.hasPrefix("comappleiwork") { + continue + } + // Skip adding item if the collection already has it to prevent duplicates if collection.contains(itemURL) { continue } @@ -403,18 +381,23 @@ func findPathsForApp(appState: AppState, locations: Locations) { collection.append(itemURL) } } else { - // Catch all the com.apple folders in the OS since there's a ton and probably unrelated - if itemL.contains("comapple") { + // Catch Xcode files for Xcodes app + if itemL.contains("xcode") && bundleIdentifierL.contains("comappledt") { + if filterItem.contains("comrobotsandpencilsxcodesapp") || filterItem.contains("comoneminutegamesxcodecleaner") || filterItem.contains("iohyperappxcodecleaner") || filterItem.contains("xcodesjson") { + continue + } + if itemL.contains(bundle) || itemL.contains(bundleIdentifierL) || (nameL.count < 6 && itemL.contains(nameL)) { + collection.append(itemURL) + } + } else if itemL.contains("xcodes") && bundleIdentifierL.contains("comrobotsandpencilsxcodesapp") { + if filterItem.contains("comappledt") { + continue + } if itemL.contains(bundle) || itemL.contains(bundleIdentifierL) || (nameL.count > 4 && itemL.contains(nameL)) { collection.append(itemURL) } } - // Catch Xcode files - else if itemL.contains("xcode") { - if itemL.contains(bundle) || itemL.contains(bundleIdentifierL) || (nameL.count > 4 && itemL.contains(nameL)) { - collection.append(itemURL) - } - } else { + else { if itemL.contains(bundleIdentifierL) || itemL.contains(bundle) || (nameL.count > 3 && itemL.contains(nameL) || (nameP.count > 3 && itemL.contains(nameP))) { collection.append(itemURL) } @@ -423,13 +406,10 @@ func findPathsForApp(appState: AppState, locations: Locations) { } } catch { -// writeLog(string: "Error processing location: \(location)\n\(error)") - print("Error processing location:", location, error) + printOS("Error processing location:", location, error) continue } -// try await Task.sleep(nanoseconds: 50_000_000) - dispatchGroup.leave() // Leave the dispatch group } @@ -439,15 +419,95 @@ func findPathsForApp(appState: AppState, locations: Locations) { collection.append(contentsOf: groupContainers) let sortedCollection = collection.sorted(by: { $0.absoluteString < $1.absoluteString }) -// writeLog(string: "\n\nsortedCollection: \(sortedCollection)") + + // Calculate file details (sizes and icons) + var fileSize: [URL: Int64] = [:] + var fileIcon: [URL: NSImage?] = [:] + var updatedAppInfo = appInfo + + for path in collection { + var size: Int64 + var icon: NSImage? = nil + size = totalSizeOnDisk(for: path) + icon = getIconForFileOrFolderNS(atPath: path) + + fileSize[path] = size + fileIcon[path] = icon + } // Save to appState dispatchGroup.notify(queue: .main) { + updateOnMain { - appState.paths = sortedCollection - appState.selectedItems = Set(sortedCollection) -// writeLog(string: "\n\nappStatePaths: \(appState.paths)") -// writeLog(string: "\n\nappStateSelected: \(appState.selectedItems)") + updatedAppInfo.files = sortedCollection + updatedAppInfo.fileSize = fileSize + updatedAppInfo.fileIcon = fileIcon + if !backgroundRun { + appState.appInfo = updatedAppInfo + appState.selectedItems = Set(sortedCollection) + } + appState.appInfoStore.append(updatedAppInfo) + } + } + + completion() + + } +} + + +// Load app paths on launch +func loadAllPaths(allApps: [AppInfo], appState: AppState, locations: Locations) { + updateOnMain { + appState.appInfoStore.removeAll() + } + for app in allApps { + findPathsForApp(appInfo: app, appState: appState, locations: locations, backgroundRun: true) { + } + } +} + + +// Load item in Files view +func showAppInFiles(appInfo: AppInfo, mini: Bool, appState: AppState, locations: Locations, showPopover: Binding) { + showPopover.wrappedValue = false + DispatchQueue.main.asyncAfter(deadline: .now() + 0.0) { + updateOnMain { + appState.appInfo = .empty + if let storedAppInfo = appState.appInfoStore.first(where: { $0.path == appInfo.path }) { + appState.appInfo = storedAppInfo + appState.selectedItems = Set(storedAppInfo.files) +// withAnimation(Animation.easeIn(duration: 0.3)) { + if mini { + appState.currentView = .files + showPopover.wrappedValue.toggle() + } else { + appState.currentView = .files + } +// } + } else { + updateOnMain { + appState.showProgress = true + } + // Handle the case where the appInfo is not found in the store +// withAnimation(Animation.easeIn(duration: 0.4)) { + if mini { + appState.currentView = .files + showPopover.wrappedValue.toggle() + } else { + appState.currentView = .files + } +// } + printOS("AppInfo not found in the cached store, searching again") + appState.appInfo = .empty + appState.appInfo = appInfo + findPathsForApp(appInfo: appInfo, appState: appState, locations: locations) { + updateOnMain { + appState.selectedItems = Set(appInfo.files) + appState.showProgress = false + + } + } } } @@ -456,6 +516,7 @@ func findPathsForApp(appState: AppState, locations: Locations) { } +// Get group containers func getGroupContainers(bundleURL: URL) -> [URL] { let fileManager = FileManager.default @@ -470,14 +531,14 @@ func getGroupContainers(bundleURL: URL) -> [URL] { let status = SecCodeCopySigningInformation(staticCode!, SecCSFlags(), &signingInformation) if status != errSecSuccess { - print("Failed to copy signing information. Status: \(status)") + printOS("Failed to copy signing information. Status: \(status)") return [] } guard let topDict = signingInformation as? [String: Any], let entitlementsDict = topDict["entitlements-dict"] as? [String: Any], let appGroups = entitlementsDict["com.apple.security.application-groups"] as? [String] else { -// print("No application groups to extract from entitlements for this app.") +// printOS("No application groups to extract from entitlements for this app.") return [] } @@ -488,14 +549,135 @@ func getGroupContainers(bundleURL: URL) -> [URL] { } + + +// Reverse search for leftover zombie files +func reversePathsSearch(appState: AppState, locations: Locations, completion: @escaping () -> Void = {}) { + Task(priority: .high) { + + var collection: [URL] = [] + let fileManager = FileManager.default + let dispatchGroup = DispatchGroup() + let allPaths = appState.appInfoStore.flatMap { $0.files.map { $0.path.pearFormat() } } + let allNames = appState.appInfoStore.map { $0.appName.pearFormat() } + let skipped = ["apple", "comapple", "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", "sandobxhelper", "statuskitagent", "betaenrollmentd", "contentlinkingd", "diagnosticextensionsd", "gamed", "heard", "homed", "itunescloudd", "lldb", "mds", "mediaanalysisd", "metrickitd", "mobiletimerd", "proactived", "ptpcamerad", "studentd", "talagent", "watchlistd", "apptranslocation", "xcrun", "ds_store", "caches", "crashreporter"] // Skip system folders + + // Skip locations that might not exist + for location in locations.reverse.paths { + if !fileManager.fileExists(atPath: location) { + continue + } + + dispatchGroup.enter() + + do { + + let contents = try fileManager.contentsOfDirectory(atPath: location) + + for item in contents { + + let itemURL = URL(fileURLWithPath: location).appendingPathComponent(item) + let itemName = ("\(item)").pearFormat() + let itemPath = "\(location)/\(itemName)".pearFormat() + + if skipped.contains(where: { itemName.contains($0) }) { + continue + } + + if allPaths.contains(where: { $0 == itemPath }) || allNames.contains(where: { $0 == itemName }) { + continue + } + + collection.append(itemURL) + + } + } catch { + printOS("Error processing location:", location, error) + continue + } + + dispatchGroup.leave() // Leave the dispatch group + + } + + let sortedCollection = collection.sorted(by: { $0.absoluteString < $1.absoluteString }) + + // Calculate file details (sizes and icons) + var fileSize: [URL: Int64] = [:] + var fileIcon: [URL: NSImage?] = [:] + var updatedZombieFile = ZombieFile.empty + + for path in collection { + var size: Int64 + var icon: NSImage? = nil +// size = 0 + size = totalSizeOnDisk(for: path) + icon = getIconForFileOrFolderNS(atPath: path) +// icon = nil + fileSize[path] = size + fileIcon[path] = icon + } + + // Save to appState + dispatchGroup.notify(queue: .main) { + + updateOnMain { +// updatedZombieFile.files = sortedCollection + updatedZombieFile.fileSize = fileSize + updatedZombieFile.fileIcon = fileIcon + appState.selectedZombieItems = Set(sortedCollection) + appState.zombieFile = updatedZombieFile + +// print(updatedZombieFile.fileSize.keys.count) +// print(updatedZombieFile) + + appState.showProgress = false + } + + } + + completion() + + } +} + + + + // Move files to trash using applescript/Finder so it asks for user password if needed func moveFilesToTrash(at fileURLs: [URL], completion: @escaping () -> Void = {}) { @AppStorage("settings.sentinel.enable") var sentinel: Bool = false if sentinel { launchctl(load: false) } + + var filesFinder = fileURLs + var filesSudo: [URL] = [] + + for file in fileURLs { + if isSocketFile(at: file) { + if let index = filesFinder.firstIndex(of: file) { + filesFinder.remove(at: index) + filesSudo.insert(file, at: 0) + } + } + } + + if !filesSudo.isEmpty { + // Remove socket files with rm + let filesSudoPaths = filesSudo.map { $0.path } + do { + let fileHandler = try Authorization.executeWithPrivileges("/bin/rm -f \(filesSudoPaths.joined(separator: " "))").get() + printOS(String(bytes: fileHandler.readDataToEndOfFile(), encoding: .utf8)!) + } catch { + printOS("Failed to remove socket file/s with privileges: \(error)") + } + } + + + updateOnBackground { - let posixFiles = fileURLs.map { "POSIX file \"\($0.path)\", " }.joined().dropLast(3) + let posixFiles = filesFinder.map { "POSIX file \"\($0.path)\", " }.joined().dropLast(3) let scriptSource = """ tell application \"Finder\" to delete { \(posixFiles)" } """ @@ -503,9 +685,9 @@ func moveFilesToTrash(at fileURLs: [URL], completion: @escaping () -> Void = {}) if let scriptObject = NSAppleScript(source: scriptSource) { let output: NSAppleEventDescriptor = scriptObject.executeAndReturnError(&error) if let error = error { - print("Error: \(error)") + printOS("Error: \(error)") } else if let outputString = output.stringValue { - print(outputString) + printOS(outputString) } } DispatchQueue.main.async { @@ -514,6 +696,122 @@ func moveFilesToTrash(at fileURLs: [URL], completion: @escaping () -> Void = {}) } } +//func moveFilesToTrash(at fileURLs: [URL] = [], appState: AppState, undo: Bool = false, completion: @escaping () -> Void = {}) { +// @AppStorage("settings.sentinel.enable") var sentinel: Bool = false +// if sentinel { +// launchctl(load: false) +// } +// +// let home = FileManager.default.homeDirectoryForCurrentUser.path +// +// if undo { +// for url in appState.trashedFiles { +// let file = url.lastPathComponent +// let destinationURL = url.deletingLastPathComponent() +// +// let destinationPath = destinationURL.path +// let trashPath = "\(home)/.Trash/\(file)" +// +// if !destinationPath.starts(with: home) { +// executePrivilegedCommand(launchPath: "/bin/cp", arguments: ["-R"] + [trashPath, destinationPath]) +// } else { +// let task = Process() +// task.launchPath = "/bin/cp" +// task.arguments = ["-R"] + [trashPath, destinationPath] +// +// let pipe = Pipe() +// task.standardOutput = pipe +// task.launch() +// +// task.waitUntilExit() +// } +// } +// +// AudioServicesPlaySystemSound(0xf); +// +// updateOnMain { +// appState.trashedFiles.removeAll() +// } +// +// } else { +// let privilegedURLs = fileURLs.filter { !$0.path.starts(with: home) } +// +// if !privilegedURLs.isEmpty { +// let filePaths = fileURLs.map { $0.path } +// executePrivilegedCommand(launchPath: "/bin/mv", arguments: ["-f"] + filePaths + ["\(home)/.Trash"]) +// } else { +// let filePaths = fileURLs.map { $0.path } +// let task = Process() +// task.launchPath = "/bin/mv" +// task.arguments = ["-f"] + filePaths + ["\(home)/.Trash"] +// +// let pipe = Pipe() +// task.standardOutput = pipe +// task.launch() +// +// task.waitUntilExit() +// } +// +// AudioServicesPlaySystemSound(0x10); +// +// } +// +// completion() +//} + +// Audio +//drag to trash.aif +//AudioServicesPlaySystemSound(0x10); + +//poof item of dock.aif +//AudioServicesPlaySystemSound(0xf); + +//func moveFilesToTrash(at fileURLs: [URL], completion: @escaping () -> Void = {}) { +// @AppStorage("settings.sentinel.enable") var sentinel: Bool = false +// if sentinel { +// launchctl(load: false) +// } +// +// let privilegedURLs = fileURLs.filter { !$0.path.starts(with: home) } +// +// if !privilegedURLs.isEmpty { +// print("Found system files") +// // Use privileged sudo mv function +// let filePaths = privilegedURLs.map { $0.path } +// executePrivilegedCommand(launchPath: "/bin/mv", arguments: filePaths + ["\(home)/.Trash"]) +// completion() +// } else { +// // Use regular shell process for files within the user's home directory +// let filePaths = fileURLs.map { $0.path } +// let task = Process() +// task.launchPath = "/bin/mv" +// task.arguments = filePaths + ["\(home)/.Trash"] +// +// let pipe = Pipe() +// task.standardOutput = pipe +// task.launch() +// +// task.waitUntilExit() +// +// completion() +// } +//} + + +func isSocketFile(at url: URL) -> Bool { + do { + let attributes = try FileManager.default.attributesOfItem(atPath: url.path) + if let fileType = attributes[FileAttributeKey.type] as? FileAttributeType { + return fileType == .typeSocket + } + } catch { + print("Error: \(error)") + } + return false +} + + + // Undo trash action func undoTrash(appState: AppState, completion: @escaping () -> Void = {}) { @@ -541,9 +839,9 @@ func undoTrash(appState: AppState, completion: @escaping () -> Void = {}) { _ = checkAndRequestAccessibilityAccess(appState: appState) } } - print("Error: \(error)") + printOS("Error: \(error)") } else if let outputString = output.stringValue { - print(outputString) + printOS(outputString) } } DispatchQueue.main.async { @@ -574,7 +872,7 @@ func undoTrash(appState: AppState, completion: @escaping () -> Void = {}) { // do { // try task.run() // } catch { -// print("Failed to run task: \(error)") +// printOS("Failed to run task: \(error)") // return false // } // @@ -598,7 +896,7 @@ func undoTrash(appState: AppState, completion: @escaping () -> Void = {}) { // do { // try task.run() // } catch { -// print("Failed to run task: \(error)") +// printOS("Failed to run task: \(error)") // return // } // diff --git a/Pearcleaner/Logic/Utilities.swift b/Pearcleaner/Logic/Utilities.swift index 0507824..4de17bf 100644 --- a/Pearcleaner/Logic/Utilities.swift +++ b/Pearcleaner/Logic/Utilities.swift @@ -7,6 +7,7 @@ import Foundation import SwiftUI +import OSLog // Make updates on main thread func updateOnMain(_ updates: @escaping () -> Void) { @@ -110,7 +111,7 @@ func checkAndRequestFullDiskAccess(appState: AppState, skipAlert: Bool = false) // let fileURL = URL(fileURLWithPath: "/Library/Application Support/com.apple.TCC/TCC.db") // // let accessStatus = FileManager.default.isReadableFile(atPath: fileURL.path) -// print(accessStatus) +// printOS(accessStatus) // if accessStatus { // diskP = true // _ = checkAndRequestAccessibilityAccess(appState: appState) @@ -225,6 +226,12 @@ func getIconForFileOrFolder(atPath path: URL) -> Image? { return Image(nsImage: nsImage) } +func getIconForFileOrFolderNS(atPath path: URL) -> NSImage? { + let icon = NSWorkspace.shared.icon(forFile: path.path) + let nsImage = icon + return nsImage +} + // Relaunch app func relaunchApp(afterDelay seconds: TimeInterval = 0.5) -> Never { @@ -263,6 +270,14 @@ extension FileManager { } } +// --- Extend print command to also output to the Console --- +func printOS(_ items: Any..., separator: String = " ", terminator: String = "\n") { + let message = items.map { "\($0)" }.joined(separator: separator) + let log = OSLog(subsystem: "com.alienator88.Pearcleaner", category: "Application") + os_log("%@", log: log, type: .debug, message) + +} + // --- Gradient --- func schemeGradient(for colorScheme: ColorScheme) -> LinearGradient { @@ -279,8 +294,9 @@ func schemeGradient(for colorScheme: ColorScheme) -> LinearGradient { + // Get total size of folders and files using DU cli command -func totalSizeOnDisk(for paths: [URL]) -> String? { +func totalSizeOnDisk(for paths: [URL]) -> Int64 { var totalSize = 0 let process = Process() @@ -290,7 +306,7 @@ func totalSizeOnDisk(for paths: [URL]) -> String? { } let pipe = Pipe() process.standardOutput = pipe - process.standardError = FileHandle.nullDevice + process.standardError = pipe//FileHandle.nullDevice try? process.run() process.waitUntilExit() @@ -301,19 +317,24 @@ func totalSizeOnDisk(for paths: [URL]) -> String? { for line in lines { let components = line.components(separatedBy: "\t") if let sizeString = components.first, let size = Int(sizeString) { - totalSize += size * 1024 // Convert the size from kilobytes to bytes + totalSize += size } } } - + + return Int64(totalSize) * 1024 // Convert the size from kilobytes to bytes +} + +func totalSizeOnDisk(for path: URL) -> Int64 { + return totalSizeOnDisk(for: [path]) +} + +// ByteFormatter +func formatByte(size: Int64) -> String { let byteCountFormatter = ByteCountFormatter() byteCountFormatter.countStyle = .file byteCountFormatter.allowedUnits = [.useAll] - return byteCountFormatter.string(fromByteCount: Int64(totalSize)) -} - -func totalSizeOnDisk(for path: URL) -> String? { - return totalSizeOnDisk(for: [path]) + return byteCountFormatter.string(fromByteCount: size) } @@ -410,10 +431,10 @@ func uninstallPearcleaner(appState: AppState, locations: Locations) { // Get app info for Pearcleaner let appInfo = getAppInfo(atPath: Bundle.main.bundleURL) - appState.appInfo = appInfo! +// appState.appInfo = appInfo! // Find application files for Pearcleaner - findPathsForApp(appState: appState, locations: locations) + findPathsForApp(appInfo: appInfo!,appState: appState, locations: locations) // Kill Pearcleaner and tell Finder to trash the files DispatchQueue.main.asyncAfter(deadline: .now() + 2) { @@ -443,7 +464,7 @@ func ensureApplicationSupportFolderExists(appState: AppState) { // Check to make sure Application Support/Support Admin folder exists if !fileManager.fileExists(atPath: supportURL.path) { try! fileManager.createDirectory(at: supportURL, withIntermediateDirectories: true) - print("Created Application Support/Pearcleaner folder") + printOS("Created Application Support/Pearcleaner folder") } } @@ -457,7 +478,7 @@ func writeLog(string: String) { // Check if the log file exists, and create it if it doesn't if !fileManager.fileExists(atPath: logFilePath) { if !fileManager.createFile(atPath: logFilePath, contents: nil, attributes: nil) { - print("Failed to create the log file.") + printOS("Failed to create the log file.") return } } @@ -469,7 +490,7 @@ func writeLog(string: String) { fileHandle.write(ns.data(using: .utf8)!) fileHandle.closeFile() } else { - print("Error opening file for appending") + printOS("Error opening file for appending") } } } @@ -491,7 +512,7 @@ func launchctl(load: Bool) { do { try plistContent.write(to: temporaryPlistURL, atomically: false, encoding: .utf8) } catch { - print("Error writing the temporary plist file: \(error)") + printOS("Error writing the temporary plist file: \(error)") return } let task = Process() @@ -506,163 +527,56 @@ func launchctl(load: Bool) { // let data = pipe.fileHandleForReading.readDataToEndOfFile() // if let output = String(data: data, encoding: .utf8) { -// print("Output: \(output)") +// printOS("Output: \(output)") // } } } -//func unloadAgent() { -// if let plistPath = Bundle.main.path(forResource: "com.alienator88.PearcleanerMonitor", ofType: "plist") { -// var plistContent = try! String(contentsOfFile: plistPath) -// let executableURL = Bundle.main.bundleURL.appendingPathComponent("Contents/MacOS/PearcleanerMonitor") -// -// // Replace the placeholder with the actual executable path -// plistContent = plistContent.replacingOccurrences(of: "__EXECUTABLE_PATH__", with: executableURL.path) -// -// // Create a temporary plist file with the updated content -// let temporaryPlistURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("com.alienator88.PearcleanerMonitor.plist") -// -// do { -// try plistContent.write(to: temporaryPlistURL, atomically: false, encoding: .utf8) -// } catch { -// print("Error writing the temporary plist file: \(error)") -// return -// } -// let task = Process() -// task.launchPath = "/bin/launchctl" -// task.arguments = ["unload", "-w", temporaryPlistURL.path] -// -// let pipe = Pipe() -// task.standardOutput = pipe -// task.standardError = pipe -// -// task.launch() -// -//// let data = pipe.fileHandleForReading.readDataToEndOfFile() -//// if let output = String(data: data, encoding: .utf8) { -//// print("Output: \(output)") -//// } -// } -//} +func getCurrentTimestamp() -> String { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS" + return dateFormatter.string(from: Date()) +} - - - - - - - -//import FileWatcher +// Execute sudo command +//func executePrivilegedCommand(launchPath: String, arguments: [String]) { +// let task = Process() +// task.launchPath = launchPath +// task.arguments = arguments // -//// MARK: https://github.com/eonist/FileWatcher +// var authorization: AuthorizationRef? = nil +// let status = AuthorizationCreate(nil, nil, AuthorizationFlags(), &authorization) // -//func fileW() { -// let filewatcher = FileWatcher([NSString(string: "~/.Trash").expandingTildeInPath]) -// filewatcher.queue = DispatchQueue.global() -// filewatcher.callback = { event in -// print("Something happened here: " + event.path) -// } +// if status == errAuthorizationSuccess { +// if let rightName = (kAuthorizationRightExecute as NSString).utf8String { +// let item = AuthorizationItem(name: rightName, valueLength: 0, value: nil, flags: 0) +// var items = [item] +// items.withUnsafeMutableBufferPointer { bufferPointer in +// var rights = AuthorizationRights(count: UInt32(bufferPointer.count), items: bufferPointer.baseAddress) // -// filewatcher.start() -//} - - - -//extension Array where Element == URL { -// // DU shell way of getting sizes -// func totalAllocatedSize2(includingSubfolders: Bool = false) throws -> Int? { -// var totalSize = 0 -// -// for path in self { -// let process = Process() -// process.launchPath = "/usr/bin/du" -// process.arguments = ["-sk", path.path] -// -// let pipe = Pipe() -// process.standardOutput = pipe -// -// try process.run() -// process.waitUntilExit() -// -// let data = pipe.fileHandleForReading.readDataToEndOfFile() -// if let output = String(data: data, encoding: .utf8) { -// let sizeString = output.components(separatedBy: "\t").first ?? "" -// if let size = Int(sizeString) { -// totalSize += size * 1024 -// } -// } -// } -// return totalSize -// } -// -// func totalAllocatedSize(includingSubfolders: Bool = false) throws -> Int? { -// var totalSize = 0 -// -// for path in self { -// if includingSubfolders { -// guard -// let urls = FileManager.default.enumerator(at: path, includingPropertiesForKeys: nil)?.allObjects as? [URL] else { return nil } -// let pathSize = try urls.lazy.reduce(0) { -// (try $1.resourceValues(forKeys: [.totalFileAllocatedSizeKey]).totalFileAllocatedSize ?? 0) + $0 -// } -// totalSize += pathSize +// let status = AuthorizationCopyRights(authorization!, &rights, nil, AuthorizationFlags([.extendRights, .interactionAllowed]), nil) +// +// if status == errAuthorizationSuccess { +// task.launch() // } else { -// let pathSize = try FileManager.default.contentsOfDirectory(at: path, includingPropertiesForKeys: nil).lazy.reduce(0) { -// (try $1.resourceValues(forKeys: [.totalFileAllocatedSizeKey]) -// .totalFileAllocatedSize ?? 0) + $0 -// } -// totalSize += pathSize +// printOS("Authorization failed.") // } // } -// -// return totalSize +// } else { +// printOS("Failed to convert rightName to C-string.") // } -// -// func totalSizeOnDisk(includingSubfolders: Bool = false) throws -> String? { -// if let totalSize = try self.totalAllocatedSize(includingSubfolders: includingSubfolders) { -// let byteCountFormatter = ByteCountFormatter() -// byteCountFormatter.countStyle = .file -// byteCountFormatter.allowedUnits = [.useBytes, .useKB, .useMB, .useTB] -// return byteCountFormatter.string(fromByteCount: Int64(totalSize)) -// } -// return nil +// } else { +// printOS("Authorization creation failed.") // } //} +// executePrivilegedCommand(launchPath: "/bin/mv", arguments: ["-f"] + filesSudoPaths + ["\(home)/.Trash"]) - - -//func isSymbolicLink(atPath path: URL) -> Bool { -//// let fileManager = FileManager.default -// -// do { -//// let path = "/Applications/Safari.app" -//// let url = URL(fileURLWithPath: path) -// let destinationPath = path.resolvingSymlinksInPath().path -// print("The symlink at \(path) points to \(destinationPath)") -// return true -// } catch { -// print("An error occurred: \(error)") -// return false -// } -// return false -//// var isDirectory = false -//// let exists = FileManager.default.fileExists(atPath: path, isDirectory: isDirectory) -//// if exists && isDirectory { -//// do { -//// let _ = try FileManager.default.destinationOfSymbolicLink(atPath: path) -//// return true -//// } catch { -//// return false -//// } -//// } -//// return false -//} diff --git a/Pearcleaner/PearcleanerApp.swift b/Pearcleaner/PearcleanerApp.swift index 481e3d8..f293c80 100644 --- a/Pearcleaner/PearcleanerApp.swift +++ b/Pearcleaner/PearcleanerApp.swift @@ -72,7 +72,7 @@ struct PearcleanerApp: App { } else { appState.currentView = .empty } - + // Disable tabbing NSWindow.allowsAutomaticWindowTabbing = false @@ -85,13 +85,11 @@ struct PearcleanerApp: App { appState.sortedApps.userApps = sortedApps.userApps appState.sortedApps.systemApps = sortedApps.systemApps - // Load progressbar total -// appState.progressManager.total = Double(locations.apps.paths.count) - - - Task { + // Find all app paths on load + loadAllPaths(allApps: sortedApps.userApps + sortedApps.systemApps, appState: appState, locations: locations) + #if !DEBUG // Make sure App Support folder exists in the future if needed for storage diff --git a/Pearcleaner/Settings/General.swift b/Pearcleaner/Settings/General.swift index 1982062..c9a6d6f 100644 --- a/Pearcleaner/Settings/General.swift +++ b/Pearcleaner/Settings/General.swift @@ -102,7 +102,8 @@ struct GeneralSettingsTab: View { RoundedRectangle(cornerRadius: 8) .fill(Color("mode").opacity(0.05)) ) - + + HStack { VStack(alignment: .leading, spacing: 5) { Text("Mini").font(.title2) @@ -117,11 +118,10 @@ struct GeneralSettingsTab: View { .onChange(of: mini) { newVal in if mini { resizeWindowAuto(windowSettings: windowSettings) -// resizeWindow(width: 300, height: 300) - appState.currentView = miniView ? .apps : .empty +// showPopover = false +// appState.currentView = miniView ? .apps : .empty } else { resizeWindowAuto(windowSettings: windowSettings) -// resizeWindow(width: 700, height: 500) if appState.appInfo.appName.isEmpty { appState.currentView = .empty } else { @@ -139,7 +139,7 @@ struct GeneralSettingsTab: View { HStack { VStack(alignment: .leading, spacing: 5) { - Text("Mini - Default View").font(.title2) + Text("Mini - \(miniView ? "Apps List" : "Drop Target")").font(.title2) Text("Toggles drop target or apps list view on launch") .font(.footnote) .foregroundStyle(.gray) @@ -160,7 +160,7 @@ struct GeneralSettingsTab: View { HStack { VStack(alignment: .leading, spacing: 5) { - Text("Mini - Popover").font(.title2) + Text("Mini - \(popoverStay ? "Popover on Top" : "Popover not on Top")").font(.title2) Text("Keeps file search popover on top in mini mode") .font(.footnote) .foregroundStyle(.gray) diff --git a/Pearcleaner/Settings/SettingsWindow.swift b/Pearcleaner/Settings/SettingsWindow.swift index fd820f3..626a021 100644 --- a/Pearcleaner/Settings/SettingsWindow.swift +++ b/Pearcleaner/Settings/SettingsWindow.swift @@ -9,7 +9,7 @@ import SwiftUI struct SettingsView: View { @EnvironmentObject var appState: AppState - + var body: some View { TabView() { diff --git a/Pearcleaner/Settings/Update.swift b/Pearcleaner/Settings/Update.swift index c586e33..cc60274 100644 --- a/Pearcleaner/Settings/Update.swift +++ b/Pearcleaner/Settings/Update.swift @@ -22,6 +22,12 @@ struct Asset: Codable { let browser_download_url: String } +extension Release { + var modifiedBody: String { + return body.replacingOccurrences(of: "- [x]", with: "􀆊").replacingOccurrences(of: "###", with: "") + } +} + struct UpdateSettingsTab: View { @EnvironmentObject var appState: AppState @@ -46,7 +52,7 @@ struct UpdateSettingsTab: View { ForEach(appState.releases, id: \.id) { release in VStack(alignment: .leading) { LabeledDivider(label: "\(release.tag_name)") - Text(release.body) + Text(release.modifiedBody) } } @@ -99,15 +105,16 @@ func loadGithubReleases(appState: AppState, manual: Bool = false) { if let data = data { if let decodedResponse = try? JSONDecoder().decode([Release].self, from: data) { DispatchQueue.main.async { - let lastFiveReleases = Array(decodedResponse.prefix(3)) // Get only the last 3 recent releases - appState.releases = lastFiveReleases + let lastFewReleases = Array(decodedResponse.prefix(3)) // Get only the last 3 recent releases + appState.releases = lastFewReleases + checkForUpdate(appState: appState, manual: manual) } return } } - print("Fetch failed: \(error?.localizedDescription ?? "Unknown error")") + printOS("Fetch failed: \(error?.localizedDescription ?? "Unknown error")") }.resume() } @@ -172,7 +179,7 @@ func downloadUpdate(appState: AppState) { } catch { - print("Error moving downloaded file: \(error.localizedDescription)") + printOS("Error moving downloaded file: \(error.localizedDescription)") } } @@ -233,7 +240,7 @@ func UnzipAndReplace(DownloadedFileURL fileURL: String, appState: AppState) { } catch { - print("Error updating the app: \(error)") + printOS("Error updating the app: \(error)") } } diff --git a/Pearcleaner/Views/AppListItems.swift b/Pearcleaner/Views/AppListItems.swift index 71ced94..3f60619 100644 --- a/Pearcleaner/Views/AppListItems.swift +++ b/Pearcleaner/Views/AppListItems.swift @@ -21,7 +21,7 @@ struct AppListItems: View { let itemId = UUID() let appInfo: AppInfo var isSelected: Bool { - appState.appInfo == appInfo + appState.appInfo.path == appInfo.path } var body: some View { @@ -71,7 +71,7 @@ struct AppListItems: View { // .background(Color("mode").opacity(0.1)) // .clipShape(.capsule) } - + Text(appInfo.appVersion) .font(.footnote) .foregroundStyle(Color("mode").opacity(0.5)) @@ -95,25 +95,9 @@ struct AppListItems: View { } } .onTapGesture { - showPopover = false - DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { - updateOnMain { - appState.appInfo = .empty - appState.appInfo = appInfo -// appState.progressManager.resetProgress() - findPathsForApp(appState: appState, locations: locations) - withAnimation(Animation.easeIn(duration: 0.4)) { - if mini { - showPopover.toggle() - } else { - appState.currentView = .files - } - } - } - - } - + showAppInFiles(appInfo: appInfo, mini: mini, appState: appState, locations: locations, showPopover: $showPopover) } + // .popover(isPresented: Binding( // get: { showPopover && appState.appInfo.id == appInfo.id}, // set: { _ in showPopover = false} diff --git a/Pearcleaner/Views/AppListView.swift b/Pearcleaner/Views/AppListView.swift index c6781ba..a150824 100644 --- a/Pearcleaner/Views/AppListView.swift +++ b/Pearcleaner/Views/AppListView.swift @@ -45,7 +45,7 @@ struct AppListView: View { if appState.reload { VStack { Spacer() - ProgressView("Refreshing applications") + ProgressView("Loading apps and files") Spacer() } .frame(width: sidebarWidth) @@ -84,7 +84,7 @@ struct AppListView: View { if filteredUserApps.count > 0 { VStack { - Header(title: "User", count: filteredUserApps.count) + Header(title: "User", count: filteredUserApps.count, showPopover: $showPopover) ForEach(filteredUserApps, id: \.self) { appInfo in AppListItems(search: $search, showPopover: $showPopover, appInfo: appInfo) if appInfo != filteredUserApps.last { @@ -98,7 +98,7 @@ struct AppListView: View { if filteredSystemApps.count > 0 { VStack { - Header(title: "System", count: filteredSystemApps.count) + Header(title: "System", count: filteredSystemApps.count, showPopover: $showPopover) ForEach(filteredSystemApps, id: \.self) { appInfo in AppListItems(search: $search, showPopover: $showPopover, appInfo: appInfo) if appInfo != filteredSystemApps.last { @@ -133,12 +133,16 @@ struct AppListView: View { // Details View VStack(spacing: 0) { if appState.currentView == .empty || appState.currentView == .apps { - TopBar() + TopBar(showPopover: $showPopover) AppDetailsEmptyView(showPopover: $showPopover) } else if appState.currentView == .files { - TopBar() + TopBar(showPopover: $showPopover) FilesView(showPopover: $showPopover, search: $search) .id(appState.appInfo.id) + } else if appState.currentView == .zombie { + TopBar(showPopover: $showPopover) + ZombieView(showPopover: $showPopover, search: $search) + .id(appState.appInfo.id) } } // .padding(.leading, appState.sidebar ? 0 : 10) @@ -161,10 +165,10 @@ struct AppListView: View { // // // Check if the file URL has a ".app" extension // // if url.pathExtension.lowercased() == "app" { // // // Handle the dropped file URL here -// // print("Dropped .app file URL: \(url)") +// // printOS("Dropped .app file URL: \(url)") // // } else { // // // Print a message for non-.app files -// // print("Unsupported file type. Only .app files are accepted.") +// // printOS("Unsupported file type. Only .app files are accepted.") // // } // } // } @@ -240,7 +244,8 @@ struct Header: View { let count: Int @State private var hovered = false @EnvironmentObject var appState: AppState - + @EnvironmentObject var locations: Locations + @Binding var showPopover: Bool var body: some View { HStack { @@ -259,10 +264,12 @@ struct Header: View { withAnimation { // Refresh Apps list appState.reload.toggle() + showPopover = false let sortedApps = getSortedApps() DispatchQueue.main.asyncAfter(deadline: .now() + 1) { appState.sortedApps.userApps = sortedApps.userApps appState.sortedApps.systemApps = sortedApps.systemApps + loadAllPaths(allApps: sortedApps.userApps + sortedApps.systemApps, appState: appState, locations: locations) appState.reload.toggle() } } diff --git a/Pearcleaner/Views/DropTarget.swift b/Pearcleaner/Views/DropTarget.swift index f8f61d3..106cafb 100644 --- a/Pearcleaner/Views/DropTarget.swift +++ b/Pearcleaner/Views/DropTarget.swift @@ -125,24 +125,58 @@ struct DropTarget: View, DropDelegate { return } guard let url = URL(dataRepresentation: data, relativeTo: nil) else { - print("Error: Not a valid URL.") + printOS("Error: Not a valid URL.") return } if url.pathExtension == "app" { let appInfo = getAppInfo(atPath: url) - - DispatchQueue.main.async { - self.ants = false - appState.appInfo = appInfo! - findPathsForApp(appState: appState, locations: locations) - if mini { - showPopover = true - } else { - appState.currentView = .files - } - } +// showPopover = false + self.ants = false + showAppInFiles(appInfo: appInfo!, mini: mini, appState: appState, locations: locations, showPopover: $showPopover) + +// DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { +// updateOnMain { +// appState.appInfo = .empty +// if let storedAppInfo = appState.appInfoStore.first(where: { $0.path == appInfo!.path }) { +// appState.appInfo = storedAppInfo +// // appState.paths = storedAppInfo.fileSize.keys.map { $0 }//storedAppInfo.files +// appState.selectedItems = Set(storedAppInfo.files) +// withAnimation(Animation.easeIn(duration: 0.4)) { +// if mini { +// showPopover.toggle() +// } else { +// appState.currentView = .files +// } +// } +// } else { +// // Handle the case where the appInfo is not found in the store +// printOS("AppInfo not found in the cached store, searching again") +// appState.appInfo = .empty +// appState.appInfo = appInfo! +// findPathsForApp(appInfo: appInfo!, appState: appState, locations: locations) +// withAnimation(Animation.easeIn(duration: 0.4)) { +// if mini { +// showPopover.toggle() +// } else { +// appState.currentView = .files +// } +// } +// } +// } +// +// } +// DispatchQueue.main.async { +// self.ants = false +// appState.appInfo = appInfo! +// findPathsForApp(appState: appState, locations: locations) +// if mini { +// showPopover = true +// } else { +// appState.currentView = .files +// } +// } } else { - print("Error: Dropped file is not an application bundle") + printOS("Error: Dropped file is not an application bundle") DispatchQueue.main.async { self.shouldFlash = true } diff --git a/Pearcleaner/Views/FilesView.swift b/Pearcleaner/Views/FilesView.swift index 7c9cddf..dab7559 100644 --- a/Pearcleaner/Views/FilesView.swift +++ b/Pearcleaner/Views/FilesView.swift @@ -11,25 +11,24 @@ import SwiftUI struct FilesView: View { @EnvironmentObject var appState: AppState @EnvironmentObject var locations: Locations - @State private var appSize: String = "" - @State private var showDetails: Bool = false @State private var showPop: Bool = false - @State private var itemDetails: [(size: String, icon: Image?)] = [] @AppStorage("settings.general.mini") private var mini: Bool = false @AppStorage("settings.sentinel.enable") private var sentinel: Bool = false @Environment(\.colorScheme) var colorScheme @Binding var showPopover: Bool @Binding var search: String + @State private var selectedOption = "Default" + @State private var toggles: Bool = true var body: some View { VStack(alignment: .center) { - if !self.showDetails { + if appState.showProgress { //!self.showDetails { VStack { Spacer() // ProgressView("Finding application files..") // .progressViewStyle(.linear) // Spacer() - Text("Finding application files..").font(.title3) + Text("Almost there, still gathering files..").font(.title3) .foregroundStyle((.gray.opacity(0.8))) ProgressView() .progressViewStyle(.linear) @@ -71,8 +70,9 @@ struct FilesView: View { Spacer() - Text("\(appSize)").font(.title).fontWeight(.bold) -// .foregroundStyle(Color("AccentColor")) + Text("\(formatByte(size: appState.appInfo.totalSize))").font(.title).fontWeight(.bold) + + } @@ -84,7 +84,7 @@ struct FilesView: View { // .foregroundStyle(Color("AccentColor")) .opacity(0.8) Spacer() - Text("\(appState.paths.count > 1 ? "\(appState.paths.count) items" : "\(appState.paths.count) item")").font(.callout) + Text("\(appState.appInfo.fileSize.count > 1 ? "\(appState.appInfo.fileSize.count) items" : "\(appState.appInfo.fileSize.count) item")").font(.callout) // .foregroundStyle(Color("AccentColor").opacity(0.7)) .underline() } @@ -142,31 +142,70 @@ struct FilesView: View { ) - + HStack(alignment: .center) { + Spacer() + + Text("\(toggles ? "Selected: All" : "Selected: None")").font(.subheadline) + Toggle("", isOn: $toggles) + .onChange(of: toggles) { value in + if value { + updateOnMain { + appState.selectedItems = Set(appState.appInfo.files) + } + } else { + updateOnMain { + appState.selectedItems.removeAll() + } + } + } + } + .padding(.top) + ScrollView() { VStack { - ForEach(Array(zip(appState.paths, itemDetails)), id: \.0) { path, details in - if let firstPath = appState.paths.first, path == firstPath { - FileDetailsItem(size: details.size, icon: details.icon, path: path) - .padding(.trailing) - Divider().padding(.leading, 40).padding(.trailing) - } else { + let sortedFilesSize = appState.appInfo.files.sorted(by: { appState.appInfo.fileSize[$0, default: 0] > appState.appInfo.fileSize[$1, default: 0] }) + +// let sortedFilesAlpha = appState.appInfo.files.sorted(by: { $0.lastPathComponent < $1.lastPathComponent }) + + let sort = selectedOption == "Default" ? appState.appInfo.files : sortedFilesSize + + ForEach(sort, id: \.self) { path in + if let fileSize = appState.appInfo.fileSize[path], let fileIcon = appState.appInfo.fileIcon[path] { + let iconImage = fileIcon.map(Image.init(nsImage:)) VStack { - FileDetailsItem(size: details.size, icon: details.icon, path: path) - if path != appState.paths.last { + FileDetailsItem(size: fileSize, icon: iconImage, path: path) + if path != appState.appInfo.files.last { Divider().padding(.leading, 40) } } - .padding(.leading, 47).padding(.trailing) - - +// .padding(.leading, 47).padding(.trailing) +// if let firstPath = appState.appInfo.files.first, path == firstPath { +// FileDetailsItem(size: fileSize, icon: iconImage, path: path) +// .padding(.trailing) +// Divider().padding(.leading, 40).padding(.trailing) +// } else { +// VStack { +// FileDetailsItem(size: fileSize, icon: iconImage, path: path) +// if path != appState.appInfo.files.last { +// Divider().padding(.leading, 40) +// } +// } +// .padding(.leading, 47).padding(.trailing) +// } } } } } - .padding() + .padding([.bottom]) HStack() { + Picker(selection: $selectedOption, label: Text("Sort")) { + Text("Default").tag("Default") + Text("Size").tag("Size") + } + .pickerStyle(SegmentedPickerStyle()) + .frame(width: 150) + Spacer() if mini { @@ -186,8 +225,8 @@ struct FilesView: View { Task { updateOnMain { appState.appInfo = AppInfo.empty + search = "" if mini { - search = "" appState.currentView = .apps showPopover = false } else { @@ -195,15 +234,32 @@ struct FilesView: View { } } - let selectedItemsArray = Array(appState.selectedItems) + var selectedItemsArray = Array(appState.selectedItems) .filter { !$0.path.contains(".Trash") } .map { path in return path.path.contains("Wrapper") ? path.deletingLastPathComponent().deletingLastPathComponent() : path } + if let url = URL(string: appState.appInfo.path.absoluteString) { + let appFolderURL = url.deletingLastPathComponent() // Get the immediate parent directory + + if appFolderURL.path == "/Applications" || appFolderURL.path == "\(home)/Applications" { + // Do nothing, skip insertion + } else if appFolderURL.pathComponents.count > 2 { + // Insert into selectedItemsArray only if there is an intermediary folder + selectedItemsArray.insert(appFolderURL, at: 0) + } + } + + // Save trashed files for undo operation +// updateOnMain { +// appState.trashedFiles = selectedItemsArray +// } + killApp(appId: appState.appInfo.bundleIdentifier) { moveFilesToTrash(at: selectedItemsArray) { withAnimation { + showPopover = false updateOnMain { appState.isReminderVisible.toggle() if sentinel { @@ -230,49 +286,10 @@ struct FilesView: View { } } -// .frame(minWidth: 500, minHeight: 500) - .onAppear { - Task { - calculateFileDetails() - } - } } - - func calculateFileDetails() { - if appState.paths.count != 0 { - itemDetails = Array(repeating: (size: "", icon: nil), count: appState.paths.count) - Task { - for (index, path) in appState.paths.enumerated() { - var size = "" - var icon: Image? = nil - - if let appSize = totalSizeOnDisk(for: path) { - size = "\(appSize)" - } else { - print("Error calculating the total size on disk for item \(index).") - } - if let folderIcon = getIconForFileOrFolder(atPath: path) { - icon = folderIcon - } - itemDetails[index] = (size: size, icon: icon) - } - if let appSize = totalSizeOnDisk(for: appState.paths) { - self.appSize = "\(appSize)" - self.showDetails = true - } else { - print("Error calculating the total size on disk.") - } - } - } else { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - calculateFileDetails() - } - } - } - - func refreshAppList(_ appInfo: AppInfo) { + showPopover = false let sortedApps = getSortedApps() updateOnMain { appState.sortedApps.userApps = [] @@ -280,7 +297,9 @@ struct FilesView: View { appState.sortedApps.userApps = sortedApps.userApps appState.sortedApps.systemApps = sortedApps.systemApps } - + Task(priority: .high){ + loadAllPaths(allApps: sortedApps.userApps + sortedApps.systemApps, appState: appState, locations: locations) + } } } @@ -288,16 +307,10 @@ struct FilesView: View { struct FileDetailsItem: View { @EnvironmentObject var appState: AppState - let size: String + let size: Int64? let icon: Image? let path: URL -// init(size: String, icon: Image?, path: URL) { -// self.size = size -// self.icon = icon -// self.path = path.path.contains("Wrapper") ? path.deletingLastPathComponent().deletingLastPathComponent() : path -// } - var body: some View { HStack(alignment: .center, spacing: 20) { @@ -306,19 +319,31 @@ struct FileDetailsItem: View { set: { isChecked in if isChecked { self.appState.selectedItems.insert(self.path) - if self.path == appState.appInfo.path { - self.appState.paths.forEach { self.appState.selectedItems.insert($0) } - } } else { self.appState.selectedItems.remove(self.path) - if self.path == appState.appInfo.path { - self.appState.selectedItems.forEach { self.appState.selectedItems.remove($0) } - } } } +// get: { self.appState.selectedItems.contains(self.path) }, +// set: { isChecked in +// if isChecked { +// self.appState.selectedItems.insert(self.path) +// if self.path == appState.appInfo.path { +// self.appState.appInfo.fileSize.keys.forEach { +// self.appState.selectedItems.insert($0) +// } +// } +// } else { +// self.appState.selectedItems.remove(self.path) +// if self.path == appState.appInfo.path { +// self.appState.selectedItems.forEach { +// self.appState.selectedItems.remove($0) +// } +// } +// } +// } )) .disabled(self.path.path.contains(".Trash")) - + if let appIcon = icon { appIcon .resizable() @@ -343,12 +368,10 @@ struct FileDetailsItem: View { } Spacer() - if size.isEmpty { - ProgressView().controlSize(.small) - } else { - Text(size) - } - + + Text(formatByte(size:size!)) + + Button("") { NSWorkspace.shared.selectFile(path.path, inFileViewerRootedAtPath: path.deletingLastPathComponent().path) diff --git a/Pearcleaner/Views/MiniMode.swift b/Pearcleaner/Views/MiniMode.swift index 5b05fea..0bfa097 100644 --- a/Pearcleaner/Views/MiniMode.swift +++ b/Pearcleaner/Views/MiniMode.swift @@ -30,19 +30,28 @@ struct MiniMode: View { MiniEmptyView(showPopover: $showPopover) } else if appState.currentView == .files { TopBarMini(search: $search, showPopover: $showPopover) - FilesView(showPopover: $showPopover, search: $search) - .id(appState.appInfo.id) + MiniAppView(search: $search, showPopover: $showPopover) + } else if appState.currentView == .zombie { + TopBarMini(search: $search, showPopover: $showPopover) + MiniAppView(search: $search, showPopover: $showPopover) } else if appState.currentView == .apps { TopBarMini(search: $search, showPopover: $showPopover) MiniAppView(search: $search, showPopover: $showPopover) } + } // .padding(.leading, appState.sidebar ? 0 : 10) .transition(.move(edge: .leading)) .popover(isPresented: $showPopover, arrowEdge: .trailing) { VStack { - FilesView(showPopover: $showPopover, search: $search) - .id(appState.appInfo.id) + if appState.currentView == .files { + FilesView(showPopover: $showPopover, search: $search) + .id(appState.appInfo.id) + } else if appState.currentView == .zombie { + ZombieView(showPopover: $showPopover, search: $search) + .id(appState.appInfo.id) + } + } .interactiveDismissDisabled(popoverStay) .background( @@ -50,7 +59,7 @@ struct MiniMode: View { .fill(Color("pop")) .padding(-80) ) - .frame(minWidth: 650, minHeight: 500) + .frame(width: 650, height: 500) } @@ -73,10 +82,10 @@ struct MiniMode: View { //// // Check if the file URL has a ".app" extension //// if url.pathExtension.lowercased() == "app" { //// // Handle the dropped file URL here -//// print("Dropped .app file URL: \(url)") +//// printOS("Dropped .app file URL: \(url)") //// } else { //// // Print a message for non-.app files -//// print("Unsupported file type. Only .app files are accepted.") +//// printOS("Unsupported file type. Only .app files are accepted.") //// } // } // } @@ -178,7 +187,7 @@ struct MiniAppView: View { if appState.reload { VStack { Spacer() - ProgressView("Refreshing applications") + ProgressView("Loadings apps and files") Spacer() } .padding(.vertical) @@ -216,7 +225,7 @@ struct MiniAppView: View { if filteredUserApps.count > 0 { VStack { - Header(title: "User", count: filteredUserApps.count) + Header(title: "User", count: filteredUserApps.count, showPopover: $showPopover) ForEach(filteredUserApps, id: \.self) { appInfo in AppListItems(search: $search, showPopover: $showPopover, appInfo: appInfo) if appInfo != filteredUserApps.last { @@ -230,7 +239,7 @@ struct MiniAppView: View { if filteredSystemApps.count > 0 { VStack { - Header(title: "System", count: filteredSystemApps.count) + Header(title: "System", count: filteredSystemApps.count, showPopover: $showPopover) ForEach(filteredSystemApps, id: \.self) { appInfo in AppListItems(search: $search, showPopover: $showPopover, appInfo: appInfo) if appInfo != filteredSystemApps.last { @@ -246,7 +255,7 @@ struct MiniAppView: View { } .scrollIndicators(.never) - if appState.currentView == .apps { + if appState.currentView != .empty { SearchBarMiniBottom(search: $search) } } diff --git a/Pearcleaner/Views/TopBar.swift b/Pearcleaner/Views/TopBar.swift index dd7b461..a9be0ed 100644 --- a/Pearcleaner/Views/TopBar.swift +++ b/Pearcleaner/Views/TopBar.swift @@ -15,12 +15,14 @@ struct TopBar: View { @AppStorage("settings.sentinel.enable") private var sentinel: Bool = false @AppStorage("settings.general.mini") private var mini: Bool = false @EnvironmentObject var appState: AppState - + @Binding var showPopover: Bool + @EnvironmentObject var locations: Locations + var body: some View { - HStack(alignment: .center, spacing: 20) { - + HStack(alignment: .center, spacing: 10) { + Spacer() - + if appState.isReminderVisible { Text("CMD + Z to undo") .font(.title2) @@ -34,10 +36,10 @@ struct TopBar: View { } } } - + Spacer() - - if appState.currentView != .empty || appState.currentView != .apps { + + if appState.currentView != .empty {//|| appState.currentView != .apps { Button("") { withAnimation(.easeInOut(duration: 0.5)) { appState.currentView = .empty @@ -45,8 +47,30 @@ struct TopBar: View { } } .buttonStyle(SimpleButtonStyle(icon: "house", help: "Home", color: Color("mode"))) + } - + + if appState.currentView != .zombie { + Button("") { + withAnimation(.easeInOut(duration: 0.5)) { + updateOnMain { + if appState.zombieFile.fileSize.keys.count == 0 { + appState.currentView = .zombie + appState.showProgress.toggle() + showPopover.toggle() + reversePathsSearch(appState: appState, locations: locations) + } else { + appState.currentView = .zombie + showPopover.toggle() + } + } + + } + } + .buttonStyle(SimpleButtonStyle(icon: "clock.arrow.circlepath", help: "Leftover Files", color: Color("mode"))) + } + + // Spacer() diff --git a/Pearcleaner/Views/TopBarMini.swift b/Pearcleaner/Views/TopBarMini.swift index d738e7a..d372e61 100644 --- a/Pearcleaner/Views/TopBarMini.swift +++ b/Pearcleaner/Views/TopBarMini.swift @@ -15,6 +15,7 @@ struct TopBarMini: View { @Binding var search: String @Binding var showPopover: Bool @EnvironmentObject var appState: AppState + @EnvironmentObject var locations: Locations var body: some View { HStack(alignment: .center, spacing: 5) { @@ -42,28 +43,29 @@ struct TopBarMini: View { } } .buttonStyle(SimpleButtonStyle(icon: "list.dash", help: "Apps List", color: Color("mode"))) - } else if appState.currentView == .files { - Button("") { - withAnimation(.easeInOut(duration: 0.5)) { - // updateOnMain { - appState.currentView = .empty - appState.appInfo = AppInfo.empty - // } + } +// else if appState.currentView == .files { +// Button("") { +// withAnimation(.easeInOut(duration: 0.5)) { +// // updateOnMain { +// appState.currentView = .empty +// appState.appInfo = AppInfo.empty +// // } +// +// } +// } +// .buttonStyle(SimpleButtonStyle(icon: "plus.square.dashed", help: "Drop Target", color: Color("mode"))) +// Button("") { +// withAnimation(.easeInOut(duration: 0.5)) { +// // updateOnMain { +// appState.currentView = .apps +// // } +// } +// } +// .buttonStyle(SimpleButtonStyle(icon: "list.dash", help: "Apps List", color: Color("mode"))) +// } - } - } - .buttonStyle(SimpleButtonStyle(icon: "plus.square.dashed", help: "Drop Target", color: Color("mode"))) - Button("") { - withAnimation(.easeInOut(duration: 0.5)) { - // updateOnMain { - appState.currentView = .apps - // } - } - } - .buttonStyle(SimpleButtonStyle(icon: "list.dash", help: "Apps List", color: Color("mode"))) - } - - if appState.currentView == .apps { + if appState.currentView != .empty { HStack { Spacer() @@ -84,22 +86,24 @@ struct TopBarMini: View { } .buttonStyle(SimpleButtonStyle(icon: "plus.square.dashed", help: "Drop Target", color: Color("mode"))) -// Spacer() + Button("") { + withAnimation(.easeInOut(duration: 0.5)) { + updateOnMain { + if appState.zombieFile.fileSize.keys.count == 0 { + appState.currentView = .zombie + appState.showProgress.toggle() + showPopover.toggle() + reversePathsSearch(appState: appState, locations: locations) + } else { + appState.currentView = .zombie + showPopover.toggle() + } + } + + } + } + .buttonStyle(SimpleButtonStyle(icon: "clock.arrow.circlepath", help: "Leftover Files", color: Color("mode"))) -// Button("") { -// withAnimation { -// // Refresh Apps list -// appState.reload.toggle() -// let sortedApps = getSortedApps() -// DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { -// appState.sortedApps.userApps = sortedApps.userApps -// appState.sortedApps.systemApps = sortedApps.systemApps -// appState.reload.toggle() -// } -// } -// } -// .buttonStyle(SimpleButtonStyle(icon: "arrow.circlepath", help: "Refresh app list", color: Color("mode"))) -// .padding(.leading, 5) } } @@ -127,10 +131,11 @@ struct SearchBarMini: View { struct SearchBarMiniBottom: View { @Binding var search: String + @EnvironmentObject var appState: AppState var body: some View { HStack { - TextField("Search", text: $search) + TextField(" Search", text: $search) .textFieldStyle(SimpleSearchStyle(trash: true, text: $search)) } .padding(.horizontal) diff --git a/Pearcleaner/Views/ZombieView.swift b/Pearcleaner/Views/ZombieView.swift new file mode 100644 index 0000000..71a96d9 --- /dev/null +++ b/Pearcleaner/Views/ZombieView.swift @@ -0,0 +1,310 @@ +// +// ZombieView.swift +// Pearcleaner +// +// Created by Alin Lupascu on 2/26/24. +// + +import Foundation +import SwiftUI + +struct ZombieView: View { + @EnvironmentObject var appState: AppState + @EnvironmentObject var locations: Locations + @State private var showPop: Bool = false + @AppStorage("settings.general.mini") private var mini: Bool = false + @AppStorage("settings.sentinel.enable") private var sentinel: Bool = false + @Environment(\.colorScheme) var colorScheme + @Binding var showPopover: Bool + @Binding var search: String + @State private var toggles: Bool = true + + var body: some View { + VStack(alignment: .center) { + if appState.showProgress { //!self.showDetails { + VStack { + Spacer() + // ProgressView("Finding application files..") + // .progressViewStyle(.linear) + // Spacer() + Text("Gathering leftover files, this might take a while..").font(.title3) + .foregroundStyle((.gray.opacity(0.8))) + ProgressView() + .progressViewStyle(.linear) + .frame(width: 400, height: 10) + + Spacer() + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .transition(.opacity) + } else { + VStack() { + + // Main Group + HStack() { + //icon +// if let appIcon = appState.appInfo.appIcon { +// Image(nsImage: appIcon) +// .resizable() +// .scaledToFit() +// // .aspectRatio(contentMode: .fit) +// .frame(width: 50, height: 50) +// .clipShape(RoundedRectangle(cornerRadius: 8)) +// .padding(.leading) +// } + //app title, size and items + VStack(alignment: .center) { + HStack(alignment: .center) { + VStack(alignment: .leading, spacing: 5){ + HStack(alignment: .center) { + Text("Leftover Files").font(.title).fontWeight(.bold) + // .foregroundStyle(Color("AccentColor")) +// Text("•").foregroundStyle(Color("AccentColor")) +// Text("\(appState.appInfo.appVersion)").font(.title3) + // .foregroundStyle(.gray.opacity(0.8)) + } +// Text("\(appState.appInfo.bundleIdentifier)").font(.title3) +// .foregroundStyle((.gray.opacity(0.8))) + } + + Spacer() + + Text("\(formatByte(size: appState.zombieFile.totalSize))").font(.title).fontWeight(.bold) + + + } + + + Divider().padding(.bottom, 7) + + HStack{ + Text("Files and folders remaining from previously installed applications") + .font(.callout) + // .foregroundStyle(Color("AccentColor")) + .opacity(0.8) + Spacer() + Text("\(appState.zombieFile.fileSize.count > 1 ? "\(appState.zombieFile.fileSize.count) items" : "\(appState.zombieFile.fileSize.count) item")").font(.callout) +// .foregroundStyle(Color("AccentColor").opacity(0.7)) + .underline() + } + HStack() { + Image(systemName: "exclamationmark.triangle.fill") + .foregroundStyle(.red) + .popover(isPresented: $showPop, arrowEdge: .top) { + VStack() { + Text("Leftover file search is not 100% accurate as it doesn't have any app bundles to check against.\nThis searches for files/folders and excludes the ones that have overlap with your currently installed apps. \nMake sure to confirm files marked for removal are correct.") + .padding() + .font(.title2) + } + + } + + Text("Warning") + .foregroundStyle(Color.red) + + Spacer() + Toggle("\(toggles ? "Selected: All" : "Selected: None")", isOn: $toggles) + .controlSize(.small) +// .toggleStyle(.switch) + .onChange(of: toggles) { value in + if value { + updateOnMain { + appState.selectedZombieItems = Set(appState.zombieFile.fileSize.keys) + } + } else { + updateOnMain { + appState.selectedZombieItems.removeAll() + } + } + } + } + .padding(.top) + .onTapGesture { + showPop = true + } + + + + + } + .padding(20) + } + // .padding(.horizontal) + .background( + RoundedRectangle(cornerRadius: 8) + // .strokeBorder(Color("AccentColor"), lineWidth: 0.5) + .fill(Color("mode").opacity(colorScheme == .dark ? 0.05 : 0.05)) + // .background( + // RoundedRectangle(cornerRadius: 8) + // .strokeBorder(Color("AccentColor").opacity(colorScheme == .dark ? 0.1 : 0.1), lineWidth: 1) + // ) + ) + + + + ScrollView() { + LazyVStack { + ForEach(appState.zombieFile.fileSize.keys.sorted(by: { $0.lastPathComponent < $1.lastPathComponent }), id: \.self) { file in + if let fileSize = appState.zombieFile.fileSize[file], let fileIcon = appState.zombieFile.fileIcon[file] { + let iconImage = fileIcon.map(Image.init(nsImage:)) + + ZombieFileDetailsItem(size: fileSize, icon: iconImage, path: file) + .padding(.trailing) + + if file != appState.zombieFile.fileSize.keys.sorted(by: { $0.absoluteString < $1.absoluteString }).last { + Divider().padding(.leading, 40) + } + } + } + } + + } + .padding() + + HStack() { + Spacer() + + if mini { + Button("Close") { + updateOnMain { + appState.appInfo = AppInfo.empty + search = "" + appState.currentView = .apps + showPopover = false + } + } + } + + + Button("Rescan") { + updateOnMain { + appState.zombieFile = .empty + appState.showProgress.toggle() + reversePathsSearch(appState: appState, locations: locations) + } + } + + Button("Remove") { + Task { + updateOnMain { + appState.zombieFile = .empty + search = "" + if mini { + appState.currentView = .apps + showPopover = false + } else { + appState.currentView = .empty + } + } + + let selectedItemsArray = Array(appState.selectedZombieItems) + .filter { !$0.path.contains(".Trash") } + + killApp(appId: appState.appInfo.bundleIdentifier) { + moveFilesToTrash(at: selectedItemsArray) { + withAnimation { + showPopover = false + updateOnMain { + appState.isReminderVisible.toggle() + if sentinel { + launchctl(load: true) + } + } + } + } + } + + } + + } + .disabled(appState.selectedZombieItems.isEmpty) + } + + } + .transition(.opacity) + .padding(20) + + } + + } + } + + func refreshAppList(_ appInfo: AppInfo) { + showPopover = false + let sortedApps = getSortedApps() + updateOnMain { + appState.sortedApps.userApps = [] + appState.sortedApps.systemApps = [] + appState.sortedApps.userApps = sortedApps.userApps + appState.sortedApps.systemApps = sortedApps.systemApps + } + Task(priority: .high){ + loadAllPaths(allApps: sortedApps.userApps + sortedApps.systemApps, appState: appState, locations: locations) + } + } +} + + + +struct ZombieFileDetailsItem: View { + @EnvironmentObject var appState: AppState + let size: Int64? + let icon: Image? + let path: URL + + var body: some View { + + HStack(alignment: .center, spacing: 20) { + Toggle("", isOn: Binding( + get: { self.appState.selectedZombieItems.contains(self.path) }, + set: { isChecked in + if isChecked { + self.appState.selectedZombieItems.insert(self.path) + } else { + self.appState.selectedZombieItems.remove(self.path) + } + } + )) + + if let appIcon = icon { + appIcon + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 30, height: 30) + .clipShape(RoundedRectangle(cornerRadius: 8)) + + } + VStack(alignment: .leading, spacing: 5) { + Text(path.lastPathComponent) + .font(.title3) + .lineLimit(1) + .truncationMode(.tail) + .help(path.lastPathComponent) + Text(path.path) + .font(.footnote) + .lineLimit(2) + .truncationMode(.tail) + .opacity(0.5) + // .foregroundStyle(Color("AccentColor")) + .help(path.path) + } + + Spacer() + + Text(formatByte(size:size!)) + + + + Button("") { + NSWorkspace.shared.selectFile(path.path, inFileViewerRootedAtPath: path.deletingLastPathComponent().path) + } + .buttonStyle(SimpleButtonStyle(icon: "folder.fill", help: "Show in Finder", color: Color("mode"))) + + } + .background( + RoundedRectangle(cornerRadius: 10) + .fill(Color.white.opacity(0)) + ) + } +} + diff --git a/PearcleanerSentinel/main.swift b/PearcleanerSentinel/main.swift index abe12d6..c50f874 100644 --- a/PearcleanerSentinel/main.swift +++ b/PearcleanerSentinel/main.swift @@ -108,7 +108,7 @@ func writeLog(string: String) { // // return appFiles.map { $0.path } // } catch { -// print("Failed to get contents of trash directory: \(error)") +// printOS("Failed to get contents of trash directory: \(error)") // return [] // } //} @@ -126,13 +126,13 @@ func writeLog(string: String) { //if hasAccessToTrashFolder() { // writeLog(string: "Your tool has access to the Trash folder.") -// print("Your tool has access to the Trash folder.") +// printOS("Your tool has access to the Trash folder.") //} else { // if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles") { // NSWorkspace.shared.open(url) // } // writeLog(string: "Your tool does not have access to the Trash folder.") -// print("Your tool does not have access to the Trash folder.") +// printOS("Your tool does not have access to the Trash folder.") //} @@ -280,6 +280,6 @@ func writeLog(string: String) { // let fileManager = FileManager.default // let trashURLs = fileManager.urls(for: .trashDirectory, in: .userDomainMask) // let trashContents = try? fileManager.contentsOfDirectory(at: trashURLs.first!, includingPropertiesForKeys: nil, options: []) -// print(trashContents as Any) +// printOS(trashContents as Any) // } //}