diff --git a/Pearcleaner.xcodeproj/project.pbxproj b/Pearcleaner.xcodeproj/project.pbxproj index 58e31e3..fd3bf91 100644 --- a/Pearcleaner.xcodeproj/project.pbxproj +++ b/Pearcleaner.xcodeproj/project.pbxproj @@ -568,8 +568,8 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - APP_BUILD = 40; - APP_VERSION = 3.6.1; + APP_BUILD = 41; + APP_VERSION = 3.6.2; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; @@ -638,8 +638,8 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - APP_BUILD = 40; - APP_VERSION = 3.6.1; + APP_BUILD = 41; + APP_VERSION = 3.6.2; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; diff --git a/Pearcleaner/Logic/DeepLink.swift b/Pearcleaner/Logic/DeepLink.swift index 74e10e1..efac51a 100644 --- a/Pearcleaner/Logic/DeepLink.swift +++ b/Pearcleaner/Logic/DeepLink.swift @@ -27,6 +27,7 @@ class DeeplinkManager { if url.pathExtension == "app" { handleAppBundle(url: url, appState: appState, locations: locations) } else if url.scheme == DeepLinkConstants.scheme, + // This handles SentinelMonitor FileWatcher url.host == DeepLinkConstants.host, let components = URLComponents(url: url, resolvingAgainstBaseURL: true), let queryItems = components.queryItems { diff --git a/Pearcleaner/Logic/Logic.swift b/Pearcleaner/Logic/Logic.swift index 672f717..2ba9743 100644 --- a/Pearcleaner/Logic/Logic.swift +++ b/Pearcleaner/Logic/Logic.swift @@ -200,14 +200,14 @@ func showAppInFiles(appInfo: AppInfo, appState: AppState, locations: Locations, // Move files to trash using applescript/Finder so it asks for user password if needed func moveFilesToTrash(appState: AppState, at fileURLs: [URL], completion: @escaping (Bool) -> Void = {_ in }) { - @AppStorage("settings.sentinel.enable") var sentinel: Bool = false - if sentinel { - launchctl(load: false) - } + // Stop Sentinel FileWatcher momentarily to ignore .app bundle being sent to Trash + sendStopNotificationFW() + updateOnBackground { - let posixFiles = fileURLs.map { "POSIX file \"\($0.path)\", " }.joined().dropLast(3) + let posixFiles = fileURLs.map { item in + return "POSIX file \"\(item.path)\"" + (item == fileURLs.last ? "" : ", ")}.joined() let scriptSource = """ - tell application \"Finder\" to delete { \(posixFiles)" } + tell application \"Finder\" to delete { \(posixFiles) } """ var error: NSDictionary? diff --git a/Pearcleaner/Logic/Updater.swift b/Pearcleaner/Logic/Updater.swift index cfcbcfd..11dc073 100644 --- a/Pearcleaner/Logic/Updater.swift +++ b/Pearcleaner/Logic/Updater.swift @@ -66,10 +66,10 @@ func checkForUpdate(appState: AppState, manual: Bool = false) { func downloadUpdate(appState: AppState) { updateOnMain { - appState.progressBar.0 = "Getting update file links ready" + appState.progressBar.0 = "UPDATER: Getting update link" appState.progressBar.1 = 0.1 } - + let fileManager = FileManager.default guard let latestRelease = appState.releases.first else { return } guard let asset = latestRelease.assets.first else { return } guard let url = URL(string: asset.url) else { return } @@ -78,38 +78,37 @@ func downloadUpdate(appState: AppState) { let downloadTask = URLSession.shared.downloadTask(with: request) { localURL, urlResponse, error in updateOnMain { - appState.progressBar.0 = "Downloading update file" + appState.progressBar.0 = "UPDATER: Starting download of update file" appState.progressBar.1 = 0.2 } guard let localURL = localURL else { return } - - let fileManager = FileManager.default - let destinationURL = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!.appendingPathComponent("Pearcleaner").appendingPathComponent("\(asset.name)") + + let destinationURL = FileManager.default.temporaryDirectory.appendingPathComponent("\(asset.name)") do { if fileManager.fileExists(atPath: destinationURL.path) { try? fileManager.removeItem(at: destinationURL) } + updateOnMain { - appState.progressBar.0 = "Moving update file to Application Support" + appState.progressBar.0 = "UPDATER: File downloaded to temp directory" + appState.progressBar.1 = 0.3 + } + try fileManager.moveItem(at: localURL, to: destinationURL) + + updateOnMain { + appState.progressBar.0 = "UPDATER: File renamed using asset name" appState.progressBar.1 = 0.4 } - try fileManager.moveItem(at: localURL, to: destinationURL) - UnzipAndReplace(DownloadedFileURL: destinationURL.path, appState: appState) - updateOnMain { - appState.progressBar.0 = "Done, please restart!" - appState.progressBar.1 = 1.0 - appState.updateAvailable = false - } - - } catch { printOS("Error moving downloaded file: \(error.localizedDescription)") } + + } downloadTask.resume() @@ -122,7 +121,7 @@ func UnzipAndReplace(DownloadedFileURL fileURL: String, appState: AppState) { do { updateOnMain { - appState.progressBar.0 = "Deleting existing application" + appState.progressBar.0 = "UPDATER: Removing currently installed application bundle" appState.progressBar.1 = 0.5 } @@ -130,11 +129,10 @@ func UnzipAndReplace(DownloadedFileURL fileURL: String, appState: AppState) { try fileManager.removeItem(atPath: appBundle) updateOnMain { - appState.progressBar.0 = "Unziping new update file to original Pearcleaner location" + appState.progressBar.0 = "UPDATER: Unziping file to original install location" appState.progressBar.1 = 0.6 } - // Unzip the downloaded update file to your app's bundle path let process = Process() process.executableURL = URL(fileURLWithPath: "/usr/bin/ditto") @@ -146,13 +144,18 @@ func UnzipAndReplace(DownloadedFileURL fileURL: String, appState: AppState) { process.waitUntilExit() updateOnMain { - appState.progressBar.0 = "Deleting update file" + appState.progressBar.0 = "UPDATER: Removing file from temp directory" appState.progressBar.1 = 0.8 } // After unzipping, remove the update file try fileManager.removeItem(atPath: fileURL) + updateOnMain { + appState.progressBar.0 = "UPDATER: Completed, please restart!" + appState.progressBar.1 = 1.0 + appState.updateAvailable = false + } } catch { printOS("Error updating the app: \(error)") diff --git a/Pearcleaner/Logic/Utilities.swift b/Pearcleaner/Logic/Utilities.swift index f0e216a..c7ec78d 100644 --- a/Pearcleaner/Logic/Utilities.swift +++ b/Pearcleaner/Logic/Utilities.swift @@ -121,11 +121,13 @@ func findAndHideWindows(named titles: [String]) { } func findAndSetWindowFrame(named titles: [String], windowSettings: WindowSettings) { - for title in titles { - if let window = NSApp.windows.first(where: { $0.title == title }) { - window.isRestorable = false - let frame = windowSettings.loadWindowSettings() - window.setFrame(frame, display: true) + windowSettings.registerDefaultWindowSettings() { + for title in titles { + if let window = NSApp.windows.first(where: { $0.title == title }) { + window.isRestorable = false + let frame = windowSettings.loadWindowSettings() + window.setFrame(frame, display: true) + } } } } @@ -279,16 +281,17 @@ func killApp(appId: String, completion: @escaping () -> Void = {}) { } // Remove app from cache -func removeApp(appState: AppState, withId id: UUID) { +func removeApp(appState: AppState, withPath path: URL) { @AppStorage("settings.general.brew") var brew: Bool = false DispatchQueue.main.async { + // Remove from sortedApps if found - if let index = appState.sortedApps.firstIndex(where: { $0.id == id }) { + if let index = appState.sortedApps.firstIndex(where: { $0.path == path }) { appState.sortedApps.remove(at: index) - return // Exit the function if the app was found and removed +// return // Exit the function if the app was found and removed } // Remove from appInfoStore if found - if let index = appState.appInfoStore.firstIndex(where: { $0.id == id }) { + if let index = appState.appInfoStore.firstIndex(where: { $0.path == path }) { appState.appInfoStore.remove(at: index) } // Brew cleanup if enabled @@ -410,13 +413,13 @@ extension String { // --- Trash Relationship --- -extension FileManager { - public func isInTrash(_ file: URL) -> Bool { - var relationship: URLRelationship = .other - try? getRelationship(&relationship, of: .trashDirectory, in: .userDomainMask, toItemAt: file) - return relationship == .contains - } -} +//extension FileManager { +// public func isInTrash(_ file: URL) -> Bool { +// var relationship: URLRelationship = .other +// try? getRelationship(&relationship, of: .trashDirectory, in: .userDomainMask, toItemAt: file) +// return relationship == .contains +// } +//} // --- Extend print command to also output to the Console --- func printOS(_ items: Any..., separator: String = " ", terminator: String = "\n") { @@ -518,37 +521,6 @@ func isSupportedFileType(at path: String) -> Bool { } -// Alerts -func presentAlert(appState: AppState) -> Alert { - - switch appState.alertType { - case .update: - return Alert(title: Text("Update Available 🥳"), message: Text("You may choose to install the update now, otherwise you may check again later from Settings"), primaryButton: .default(Text("Install")) { - downloadUpdate(appState: appState) - appState.alertType = .off - }, secondaryButton: .cancel()) - case .no_update: - return Alert(title: Text("No Updates 😌"), message: Text("Pearcleaner is on the latest release available"), primaryButton: .cancel(Text("Okay")), secondaryButton: .default(Text("Force Update")) { - downloadUpdate(appState: appState) - appState.alertType = .off - }) - case .diskAccess: - return Alert(title: Text("Permissions"), message: Text("Pearcleaner requires Full Disk and Accessibility permissions. Drag the app into the Full Disk and Accessibility pane to enable or toggle On if already present."), primaryButton: .default(Text("Allow in Settings")) { - if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles") { - NSWorkspace.shared.open(url) - } - appState.alertType = .off - }, secondaryButton: .cancel(Text("Later"))) - case .restartApp: - return Alert(title: Text("Update Completed!"), message: Text("The application has been updated to the latest version, would you like to restart now?"), primaryButton: .default(Text("Restart")) { - appState.alertType = .off - relaunchApp() - }, secondaryButton: .cancel(Text("Later"))) - case .off: - return Alert(title: Text("")) - } -} - // --- Pearcleaner Uninstall -- @@ -564,16 +536,15 @@ func uninstallPearcleaner(appState: AppState, locations: Locations) { AppPathFinder(appInfo: appInfo!, appState: appState, locations: locations, completion: { // Kill Pearcleaner and tell Finder to trash the files let selectedItemsArray = Array(appState.selectedItems).filter { !$0.path.contains(".Trash") } - let posixFiles = selectedItemsArray.map { "POSIX file \"\($0.path)\", " }.joined().dropLast(3) + let posixFiles = selectedItemsArray.map { item in + return "POSIX file \"\(item.path)\"" + (item == selectedItemsArray.last ? "" : ", ")}.joined() let scriptSource = """ - tell application \"Finder\" to delete { \(posixFiles)" } + tell application \"Finder\" to delete { \(posixFiles) } """ let task = Process() task.launchPath = "/bin/sh" task.arguments = ["-c", "sleep 1; osascript -e '\(scriptSource)'"] task.launch() - - NSApp.terminate(nil) exit(0) }).findPaths() } @@ -653,7 +624,13 @@ func launchctl(load: Bool, completion: @escaping () -> Void = {}) { } +func sendStartNotificationFW() { + DistributedNotificationCenter.default().postNotificationName(Notification.Name("Pearcleaner.StartFileWatcher"), object: nil, userInfo: nil, deliverImmediately: true) +} +func sendStopNotificationFW() { + DistributedNotificationCenter.default().postNotificationName(Notification.Name("Pearcleaner.StopFileWatcher"), object: nil, userInfo: nil, deliverImmediately: true) +} func getCurrentTimestamp() -> String { diff --git a/Pearcleaner/PearcleanerApp.swift b/Pearcleaner/PearcleanerApp.swift index c013ce0..a8cdaf6 100644 --- a/Pearcleaner/PearcleanerApp.swift +++ b/Pearcleaner/PearcleanerApp.swift @@ -7,7 +7,6 @@ import SwiftUI import AppKit -//import ServiceManagement @main struct PearcleanerApp: App { @@ -111,7 +110,8 @@ struct PearcleanerApp: App { // Make sure App Support folder exists in the future if needed for storage - ensureApplicationSupportFolderExists(appState: appState) + //MARK: This is not needed any longer as the update file is stored in /tmp directory +// ensureApplicationSupportFolderExists(appState: appState) // Check for updates after app launch checkAllPermissions(appState: appState) { results in @@ -184,7 +184,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate { if UserDefaults.standard.object(forKey: "themeColor") == nil { self.appearanceChanged() } - + if menubarEnabled { findAndHideWindows(named: ["Pearcleaner"]) NSApplication.shared.setActivationPolicy(.accessory) diff --git a/Pearcleaner/Settings/General.swift b/Pearcleaner/Settings/General.swift index dbcb052..7e78d8d 100644 --- a/Pearcleaner/Settings/General.swift +++ b/Pearcleaner/Settings/General.swift @@ -198,7 +198,7 @@ struct GeneralSettingsTab: View { } } } - .buttonStyle(SimpleButtonStyle(icon: "arrow.triangle.2.circlepath", help: "Refresh permissions")) + .buttonStyle(SimpleButtonStyle(icon: "arrow.triangle.2.circlepath", label: "Refresh", help: "Refresh permissions")) .padding(.trailing, 5) } @@ -412,11 +412,7 @@ struct GeneralSettingsTab: View { private func resetUserDefaults() { isResetting = true DispatchQueue.global(qos: .background).async { - let defaults = UserDefaults.standard - let dictionary = defaults.dictionaryRepresentation() - dictionary.keys.forEach { key in - defaults.removeObject(forKey: key) - } + UserDefaults.standard.dictionaryRepresentation().keys.forEach(UserDefaults.standard.removeObject(forKey:)) DispatchQueue.main.async { isResetting = false } @@ -424,3 +420,6 @@ struct GeneralSettingsTab: View { } } + + + diff --git a/Pearcleaner/Views/AppSearchView.swift b/Pearcleaner/Views/AppSearchView.swift index 8314810..fd8246d 100644 --- a/Pearcleaner/Views/AppSearchView.swift +++ b/Pearcleaner/Views/AppSearchView.swift @@ -42,6 +42,11 @@ struct AppSearchView: View { HStack(spacing: 10) { +#if DEBUG + Image(systemName: "ant.fill") + .foregroundStyle(.orange) + .help("DEBUG") +#endif SearchBar(search: $search, darker: (mini || menubarEnabled) ? false : true, glass: glass) diff --git a/Pearcleaner/Views/FilesView.swift b/Pearcleaner/Views/FilesView.swift index 8ccebfa..de72fb2 100644 --- a/Pearcleaner/Views/FilesView.swift +++ b/Pearcleaner/Views/FilesView.swift @@ -257,22 +257,30 @@ struct FilesView: View { Spacer() - HStack() { + HStack(alignment: .center) { + + Spacer() + + if appState.appInfo.fileSize.keys.count == 0 { + Text("Sentinel Monitor found no other files to remove") + .font(.title2) + .opacity(0.5) + .padding(.top) + } Spacer() Button("\(sizeType == "Logical" ? totalSelectedSize.logical : sizeType == "Finder" ? totalSelectedSize.finder : totalSelectedSize.real)") { Task { - let selectedItemsArray = Array(appState.selectedItems) killApp(appId: appState.appInfo.bundleIdentifier) { moveFilesToTrash(appState: appState, at: selectedItemsArray) { success in - if sentinel { - launchctl(load: true) - } + + // Send Sentinel FileWatcher start notification + sendStartNotificationFW() guard success else { return @@ -296,7 +304,7 @@ struct FilesView: View { if (appState.appInfo.wrapped && selectedItemsArray.contains(where: { $0.absoluteString == appState.appInfo.path.deletingLastPathComponent().deletingLastPathComponent().absoluteString })) || (!appState.appInfo.wrapped && selectedItemsArray.contains(where: { $0.absoluteString == appState.appInfo.path.absoluteString })) { // Match found, remove the app - removeApp(appState: appState, withId: appState.appInfo.id) + removeApp(appState: appState, withPath: appState.appInfo.path) } else { // Add deleted appInfo object to trashed array appState.trashedFiles.append(appState.appInfo) diff --git a/Pearcleaner/Views/RegularMode.swift b/Pearcleaner/Views/RegularMode.swift index 1629833..824bafb 100644 --- a/Pearcleaner/Views/RegularMode.swift +++ b/Pearcleaner/Views/RegularMode.swift @@ -111,7 +111,6 @@ struct AppDetailsEmptyView: View { PearDropView() } - Spacer() Text("Drop an app here") diff --git a/Pearcleaner/Windows/WindowSettings.swift b/Pearcleaner/Windows/WindowSettings.swift index 9c5a074..1a8b525 100644 --- a/Pearcleaner/Windows/WindowSettings.swift +++ b/Pearcleaner/Windows/WindowSettings.swift @@ -17,33 +17,64 @@ class WindowSettings { @AppStorage("settings.general.mini") private var mini: Bool = false var windows: [NSWindow] = [] - func saveWindowSettings(frame: NSRect) { + func registerDefaultWindowSettings(completion: @escaping () -> Void = {}) { + let defaults = UserDefaults.standard + + // Get primary screen + let screenFrame = NSScreen.main?.visibleFrame ?? NSRect(x: 0, y: 0, width: 800, height: 600) + + // Calculate default window sizes and x/y coordinates + let defaultWidth = Float(900) // Default width for regular window + let defaultHeight = Float(600) // Default height for regular window + let defaultWidthMini = Float(300) // Default width for mini window + let defaultHeightMini = Float(370) // Default height for mini window + let defaultX = Float((screenFrame.width - CGFloat(defaultWidth)) / 2 + screenFrame.origin.x) // Default X coordinate + let defaultY = Float((screenFrame.height - CGFloat(defaultHeight)) / 2 + screenFrame.origin.y) // Default Y coordinate + + // Set defaults only if they are not already set + if defaults.object(forKey: windowWidthKey) == nil { + defaults.set(defaultWidth, forKey: windowWidthKey) + } + if defaults.object(forKey: windowHeightKey) == nil { + defaults.set(defaultHeight, forKey: windowHeightKey) + } + if defaults.object(forKey: windowWidthKeyMini) == nil { + defaults.set(defaultWidthMini, forKey: windowWidthKeyMini) + } + if defaults.object(forKey: windowHeightKeyMini) == nil { + defaults.set(defaultHeightMini, forKey: windowHeightKeyMini) + } + if defaults.object(forKey: windowXKey) == nil { + defaults.set(defaultX, forKey: windowXKey) + } + if defaults.object(forKey: windowYKey) == nil { + defaults.set(defaultY, forKey: windowYKey) + } + + completion() + + } + + // Save user window settings + func saveWindowSettings(frame: NSRect) { UserDefaults.standard.set(Float(frame.size.width), forKey: mini ? windowWidthKeyMini : windowWidthKey) UserDefaults.standard.set(Float(frame.size.height), forKey: mini ? windowHeightKeyMini : windowHeightKey) UserDefaults.standard.set(Float(frame.origin.x), forKey: windowXKey) UserDefaults.standard.set(Float(frame.origin.y), forKey: windowYKey) } + // Load default window settings or user defined settings func loadWindowSettings() -> NSRect { - - // Retrieve window size let width = CGFloat(UserDefaults.standard.float(forKey: mini ? windowWidthKeyMini : windowWidthKey)) let height = CGFloat(UserDefaults.standard.float(forKey: mini ? windowHeightKeyMini : windowHeightKey)) + let x = CGFloat(UserDefaults.standard.float(forKey: windowXKey)) + let y = CGFloat(UserDefaults.standard.float(forKey: windowYKey)) - // Set default middle position if not set in UserDefaults - var x = CGFloat(UserDefaults.standard.float(forKey: windowXKey)) - var y = CGFloat(UserDefaults.standard.float(forKey: windowYKey)) - - - if UserDefaults.standard.object(forKey: windowXKey) == nil || UserDefaults.standard.object(forKey: windowYKey) == nil { - let screenFrame = NSScreen.main?.visibleFrame ?? NSRect(x: 0, y: 0, width: 800, height: 600) - x = (screenFrame.width - width) / 2 + screenFrame.origin.x - y = (screenFrame.height - height) / 2 + screenFrame.origin.y - } return NSRect(x: x, y: y, width: width, height: height) } + // Launch new app windows on demand func newWindow(withView view: @escaping () -> V, completion: @escaping () -> Void = {}) { findAndHideWindows(named: ["Pearcleaner"]) let contentView = view diff --git a/PearcleanerSentinel/main.swift b/PearcleanerSentinel/main.swift index cfb2d06..73d0235 100644 --- a/PearcleanerSentinel/main.swift +++ b/PearcleanerSentinel/main.swift @@ -5,28 +5,48 @@ // Created by Alin Lupascu on 11/9/23. // -import Foundation + import AppKit import FileWatcher -// Start Trash monitoring -fileWatcher() +main() -// Keep alive indefinitely -while true { - sleep(1) -} +var globalFileWatcher: FileWatcher? -func fileWatcher() { +func startGlobalFileWatcher() { let home = FileManager.default.homeDirectoryForCurrentUser.path - let filewatcher = FileWatcher(["\(home)/.Trash"]) - filewatcher.queue = DispatchQueue.global() - filewatcher.callback = { event in + globalFileWatcher = FileWatcher(["\(home)/.Trash"]) + globalFileWatcher?.queue = DispatchQueue.global() + globalFileWatcher?.callback = { event in checkApp(file: event.path) } - filewatcher.start() + globalFileWatcher?.start() } +func stopGlobalFileWatcher() { + globalFileWatcher?.stop() + globalFileWatcher = nil +} + +func setupNotificationListener() { + let notificationCenter = DistributedNotificationCenter.default() + notificationCenter.addObserver(forName: Notification.Name("Pearcleaner.StartFileWatcher"), object: nil, queue: nil) { notification in + print("Received start notification") + startGlobalFileWatcher() + } + notificationCenter.addObserver(forName: Notification.Name("Pearcleaner.StopFileWatcher"), object: nil, queue: nil) { notification in + print("Received stop notification") + stopGlobalFileWatcher() + } +} + +func main() { + setupNotificationListener() + startGlobalFileWatcher() + RunLoop.main.run() +} + + func checkApp(file: String) { let app = URL(fileURLWithPath: file) let appExt = app.pathExtension @@ -45,6 +65,11 @@ func checkApp(file: String) { } } + + + + + // --- Trash Relationship --- extension FileManager { public func isInTrash(_ file: URL) -> Bool { @@ -64,7 +89,7 @@ extension FileManager { // For testing and outputing logging to file from cmd line tool -func writeLog(string: String) { +func writeLogMon(string: String) { let fileManager = FileManager.default let home = fileManager.homeDirectoryForCurrentUser.path let logFilePath = "\(home)/Downloads/monitor.txt" @@ -88,206 +113,3 @@ func writeLog(string: String) { } } } - - - - - - - - - - - - - -// let trashContents = getTrashContents() -// if !trashContents.isEmpty && trashContents.contains(event.path) { -// NSWorkspace.shared.open(URL(string: "pear://com.alienator88.Pearcleaner?path=\(event.path)")!) -// } - -//func getTrashContents() -> [String] { -// let fileManager = FileManager.default -// let trashURLs = fileManager.urls(for: .trashDirectory, in: .userDomainMask) -// do { -// let trashContents = try fileManager.contentsOfDirectory(at: trashURLs.first!, includingPropertiesForKeys: nil, options: []) -// let appFiles = trashContents.filter { $0.pathExtension == "app" } -// writeLog(string: appFiles.first!.path) -// -// return appFiles.map { $0.path } -// } catch { -// printOS("Failed to get contents of trash directory: \(error)") -// return [] -// } -//} - -//func hasAccessToTrashFolder() -> Bool { -// let fileManager = FileManager.default -// let trashURLs = fileManager.urls(for: .trashDirectory, in: .userDomainMask) -// -// if let trashURL = trashURLs.first { -// return fileManager.isReadableFile(atPath: trashURL.path) && fileManager.isWritableFile(atPath: trashURL.path) -// } -// -// return false -//} - -//if hasAccessToTrashFolder() { -// writeLog(string: "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.") -// printOS("Your tool does not have access to the Trash folder.") -//} - - -//var dirWatcher: DirMonitor? -//func fileW2() { -// let home = FileManager.default.homeDirectoryForCurrentUser.path -// let url = URL(fileURLWithPath: "\(home)/.Trash") -// dirWatcher = DirMonitor(dir: url, queue: .global()) -// if dirWatcher?.start() == true { -// NSLog("Started directory monitoring") -// } else { -// NSLog("Failed to start directory monitoring") -// } -//} -// -//class DirMonitor { -// -// init(dir: URL, queue: DispatchQueue) { -// self.dir = dir -// self.queue = queue -// } -// -// deinit { -// // The stream has a reference to us via its `info` pointer. If the -// // client releases their reference to us without calling `stop`, that -// // results in a dangling pointer. We detect this as a programming error. -// // There are other approaches to take here (for example, messing around -// // with weak, or forming a retain cycle that’s broken on `stop`), but -// // this approach: -// // -// // * Has clear rules -// // * Is easy to implement -// // * Generate a sensible debug message if the client gets things wrong -// precondition(self.stream == nil, "released a running monitor") -// // I added this log line as part of my testing of the deallocation path. -// NSLog("did deinit") -// } -// -// let dir: URL -// let queue: DispatchQueue -// -// private var stream: FSEventStreamRef? = nil -// -// func start() -> Bool { -// precondition(self.stream == nil, "started a running monitor") -// -// // Set up our context. -// // -// // `FSEventStreamCallback` is a C function, so we pass `self` to the -// // `info` pointer so that it get call our `handleUnsafeEvents(…)` -// // method. This involves the standard `Unmanaged` dance: -// // -// // * Here we set `info` to an unretained pointer to `self`. -// // * Inside the function we extract that pointer as `obj` and then use -// // that to call `handleUnsafeEvents(…)`. -// -// var context = FSEventStreamContext() -// context.info = Unmanaged.passUnretained(self).toOpaque() -// -// // Create the stream. -// // -// // In this example I wanted to show how to deal with raw string paths, -// // so I’m not taking advantage of `kFSEventStreamCreateFlagUseCFTypes` -// // or the even cooler `kFSEventStreamCreateFlagUseExtendedData`. -// -// guard let stream = FSEventStreamCreate(nil, { (stream, info, numEvents, eventPaths, eventFlags, eventIds) in -// let obj = Unmanaged.fromOpaque(info!).takeUnretainedValue() -// obj.handleUnsafeEvents(numEvents: numEvents, eventPaths: eventPaths, eventFlags: eventFlags, eventIDs: eventIds) -// }, -// &context, -// [self.dir.path as NSString] as NSArray, -// UInt64(kFSEventStreamEventIdSinceNow), -// 1.0, -// FSEventStreamCreateFlags(kFSEventStreamCreateFlagNone) -// ) else { -// return false -// } -// self.stream = stream -// -// // Now that we have a stream, schedule it on our target queue. -// -// FSEventStreamSetDispatchQueue(stream, queue) -// guard FSEventStreamStart(stream) else { -// FSEventStreamInvalidate(stream) -// self.stream = nil -// return false -// } -// return true -// } -// -// private func handleUnsafeEvents(numEvents: Int, eventPaths: UnsafeMutableRawPointer, eventFlags: UnsafePointer, eventIDs: UnsafePointer) { -// // This takes the low-level goo from the C callback, converts it to -// // something that makes sense for Swift, and then passes that to -// // `handle(events:…)`. -// // -// // Note that we don’t need to do any rebinding here because this data is -// // coming C as the right type. -// let pathsBase = eventPaths.assumingMemoryBound(to: UnsafePointer.self) -// let pathsBuffer = UnsafeBufferPointer(start: pathsBase, count: numEvents) -// let flagsBuffer = UnsafeBufferPointer(start: eventFlags, count: numEvents) -// let eventIDsBuffer = UnsafeBufferPointer(start: eventIDs, count: numEvents) -// // As `zip(_:_:)` only handles two sequences, I map over the index. -// let events = (0.. (url: URL, flags: FSEventStreamEventFlags, eventIDs: FSEventStreamEventId) in -// let path = pathsBuffer[i] -// // We set `isDirectory` to true because we only generate directory -// // events (that is, we don’t pass -// // `kFSEventStreamCreateFlagFileEvents` to `FSEventStreamCreate`. -// // This is generally the best way to use FSEvents, but if you decide -// // to take advantage of `kFSEventStreamCreateFlagFileEvents` then -// // you’ll need to code to `isDirectory` correctly. -// let url: URL = URL(fileURLWithFileSystemRepresentation: path, isDirectory: true, relativeTo: nil) -// return (url, flagsBuffer[i], eventIDsBuffer[i]) -// } -// self.handle(events: events) -// } -// -// private func handle(events: [(url: URL, flags: FSEventStreamEventFlags, eventIDs: FSEventStreamEventId)]) { -// // In this example we just print the events with get, prefixed by a -// // count so that we can see the batching in action. -// NSLog("%d", events.count) -// for (url, flags, eventID) in events { -// NSLog("%16x %8x %@", eventID, flags, url.path) -// } -// for (url, flags, _) in events { -// if flags & FSEventStreamEventFlags(kFSEventStreamEventFlagItemRemoved) != 0 { -// NSLog("Removed: \(url.path)") -// } -// if flags & FSEventStreamEventFlags(kFSEventStreamEventFlagItemCreated) != 0 { -// NSLog("Created: \(url.path)") -// } -// } -// getTrashContents() -// } -// -// func stop() { -// guard let stream = self.stream else { -// return // We accept redundant calls to `stop`. -// } -// FSEventStreamStop(stream) -// FSEventStreamInvalidate(stream) -// self.stream = nil -// } -// -// func getTrashContents() { -// let fileManager = FileManager.default -// let trashURLs = fileManager.urls(for: .trashDirectory, in: .userDomainMask) -// let trashContents = try? fileManager.contentsOfDirectory(at: trashURLs.first!, includingPropertiesForKeys: nil, options: []) -// printOS(trashContents as Any) -// } -//}