diff --git a/Pearcleaner.xcodeproj/project.pbxproj b/Pearcleaner.xcodeproj/project.pbxproj index 7aaedce..b65f824 100644 --- a/Pearcleaner.xcodeproj/project.pbxproj +++ b/Pearcleaner.xcodeproj/project.pbxproj @@ -561,8 +561,8 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - APP_BUILD = 59; - APP_VERSION = 3.8.5; + APP_BUILD = 60; + APP_VERSION = 3.8.6; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; @@ -631,8 +631,8 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - APP_BUILD = 59; - APP_VERSION = 3.8.5; + APP_BUILD = 60; + APP_VERSION = 3.8.6; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; diff --git a/Pearcleaner/Logic/AppCommands.swift b/Pearcleaner/Logic/AppCommands.swift index 7338380..709642c 100644 --- a/Pearcleaner/Logic/AppCommands.swift +++ b/Pearcleaner/Logic/AppCommands.swift @@ -109,10 +109,20 @@ struct AppCommands: Commands { saveURLsToFile(urls: appState.selectedItems, appState: appState) } } label: { - Label("Export Files", systemImage: "square.and.arrow.up") + Label("Export File Paths", systemImage: "square.and.arrow.up") } .keyboardShortcut("e", modifiers: .command) + Button + { + if !appState.appInfo.bundleIdentifier.isEmpty { + saveURLsToFile(urls: appState.selectedItems, appState: appState, copy: true) + } + } label: { + Label("Copy File Paths", systemImage: "square.and.arrow.up") + } + .keyboardShortcut("c", modifiers: [.command, .option]) + } diff --git a/Pearcleaner/Logic/AppPathsFetch.swift b/Pearcleaner/Logic/AppPathsFetch.swift index 5b644b7..9f0554a 100644 --- a/Pearcleaner/Logic/AppPathsFetch.swift +++ b/Pearcleaner/Logic/AppPathsFetch.swift @@ -156,6 +156,8 @@ class AppPathFinder { let bundleComponents = self.appInfo.bundleIdentifier.components(separatedBy: ".").compactMap { $0 != "-" ? $0.lowercased() : nil } let bundle = bundleComponents.suffix(2).joined() let nameL = self.appInfo.appName.pearFormat() + let nameLFiltered = nameL.filter { $0.isLetter } + let nameP = self.appInfo.path.lastPathComponent.replacingOccurrences(of: ".app", with: "") for condition in conditions { @@ -178,7 +180,7 @@ class AppPathFinder { return itemL.contains(bundleIdentifierL) } - return itemL.contains(bundleIdentifierL) || itemL.contains(bundle) || (nameL.count > 3 && itemL.contains(nameL)) || (nameP.count > 3 && itemL.contains(nameP)) + return itemL.contains(bundleIdentifierL) || itemL.contains(bundle) || (nameL.count > 3 && itemL.contains(nameL)) || (nameP.count > 3 && itemL.contains(nameP) || (nameLFiltered.count > 3 && itemL.contains(nameLFiltered))) } diff --git a/Pearcleaner/Logic/Conditions.swift b/Pearcleaner/Logic/Conditions.swift index 5e00810..28de50d 100644 --- a/Pearcleaner/Logic/Conditions.swift +++ b/Pearcleaner/Logic/Conditions.swift @@ -147,7 +147,7 @@ let skipConditions: [SkipCondition] = [ // Skip files/folders during leftover file search -let skipReverse = ["apple", "temporary", "btserver", "proapps", "scripteditor", "ilife", "livefsd", "siritoday", "addressbook", "animoji", "appstore", "askpermission", "callhistory", "clouddocs", "diskimages", "dock", "facetime", "fileprovider", "instruments", "knowledge", "mobilesync", "syncservices", "homeenergyd", "icloud", "icdd", "networkserviceproxy", "familycircle", "geoservices", "installation", "passkit", "sharedimagecache", "desktop", "mbuseragent", "swiftpm", "baseband", "coresimulator", "photoslegacyupgrade", "photosupgrade", "siritts", "ipod", "globalpreferences", "apmanalytics", "apmexperiment", "avatarcache", "byhost", "contextstoreagent", "mobilemeaccounts", "mobiledocuments", "mobile", "intentbuilderc", "loginwindow", "momc", "replayd", "sharedfilelistd", "clang", "audiocomponent", "csexattrcryptoservice", "livetranscriptionagent", "sandboxhelper", "statuskitagent", "betaenrollmentd", "contentlinkingd", "diagnosticextensionsd", "gamed", "heard", "homed", "itunescloudd", "lldb", "mds", "mediaanalysisd", "metrickitd", "mobiletimerd", "proactived", "ptpcamerad", "studentd", "talagent", "watchlistd", "apptranslocation", "xcrun", "ds_store", "caches", "crashreporter", "trash", "pearcleaner", "amsdatamigratortool", "arfilecache", "assistant", "chromium", "cloudkit", "webkit", "databases", "diagnostic", "cache", "gamekit", "homebrew", "logi", "microsoft", "mozilla", "sync", "google", "sentinel", "hexnode", "sentry", "tvappservices", "reminders"] +let skipReverse = ["apple", "temporary", "btserver", "proapps", "scripteditor", "ilife", "livefsd", "siritoday", "addressbook", "animoji", "appstore", "askpermission", "callhistory", "clouddocs", "diskimages", "dock", "facetime", "fileprovider", "instruments", "knowledge", "mobilesync", "syncservices", "homeenergyd", "icloud", "icdd", "networkserviceproxy", "familycircle", "geoservices", "installation", "passkit", "sharedimagecache", "desktop", "mbuseragent", "swiftpm", "baseband", "coresimulator", "photoslegacyupgrade", "photosupgrade", "siritts", "ipod", "globalpreferences", "apmanalytics", "apmexperiment", "avatarcache", "byhost", "contextstoreagent", "mobilemeaccounts", "mobiledocuments", "mobile", "intentbuilderc", "loginwindow", "momc", "replayd", "sharedfilelistd", "clang", "audiocomponent", "csexattrcryptoservice", "livetranscriptionagent", "sandboxhelper", "statuskitagent", "betaenrollmentd", "contentlinkingd", "diagnosticextensionsd", "gamed", "heard", "homed", "itunescloudd", "lldb", "mds", "mediaanalysisd", "metrickitd", "mobiletimerd", "proactived", "ptpcamerad", "studentd", "talagent", "watchlistd", "apptranslocation", "xcrun", "ds_store", "caches", "crashreporter", "trash", "pearcleaner", "amsdatamigratortool", "arfilecache", "assistant", "chromium", "cloudkit", "webkit", "databases", "diagnostic", "cache", "gamekit", "homebrew", "logi", "microsoft", "mozilla", "sync", "google", "sentinel", "hexnode", "sentry", "tvappservices", "reminders", "pbs"] diff --git a/Pearcleaner/Logic/Utilities.swift b/Pearcleaner/Logic/Utilities.swift index 8049e15..a731d22 100644 --- a/Pearcleaner/Logic/Utilities.swift +++ b/Pearcleaner/Logic/Utilities.swift @@ -61,66 +61,90 @@ func findAndSetWindowFrame(named titles: [String], windowSettings: WindowSetting // Brew cleanup func caskCleanup(app: String) { Task(priority: .high) { - let process = Process() + let formattedApp = app.lowercased().replacingOccurrences(of: " ", with: "-") + print(formattedApp) + #if arch(x86_64) let cmd = "/usr/local/bin/brew" #elseif arch(arm64) let cmd = "/opt/homebrew/bin/brew" #endif - let formattedApp = app.lowercased().replacingOccurrences(of: " ", with: "-") - process.executableURL = URL(fileURLWithPath: "/bin/zsh") -// process.arguments = ["-c", "\(cmd) uninstall --cask \"\(formattedApp)\" --force; \(cmd) cleanup"] - process.arguments = ["-c", """ - found_cask=$(\(cmd) list --cask | grep "\(formattedApp)") - if [ -n "$found_cask" ]; then - \(cmd) uninstall --cask "$found_cask" --force; - \(cmd) cleanup; - else - echo "Cask not found for \(formattedApp)"; - fi - """] - let pipe = Pipe() - process.standardOutput = pipe - process.standardError = pipe - try? process.run() - process.waitUntilExit() // Ensure the process completes - let data = pipe.fileHandleForReading.readDataToEndOfFile() - let output = (String(data: data, encoding: .utf8) ?? "") as String - printOS(output) + + let script = """ + tell application "Terminal" + activate + do script " + found_cask=$(\(cmd) list --cask | grep '\(formattedApp)'); + if [ -n \\"$found_cask\\" ]; then + clear; + \(cmd) uninstall --cask \\"$found_cask\\" --force; + \(cmd) cleanup; + else + echo \\"Cask not found for \(formattedApp)\\"; + fi" + end tell + """ + + var error: NSDictionary? + if let appleScript = NSAppleScript(source: script) { + appleScript.executeAndReturnError(&error) + } + + if let error = error { + print("AppleScript Error: \(error)") + } } } -// Print list of files locally -func saveURLsToFile(urls: Set, appState: AppState) { - let panel = NSOpenPanel() - panel.canChooseFiles = false - panel.canChooseDirectories = true - panel.allowsMultipleSelection = false - panel.prompt = "Select Folder" - if panel.runModal() == .OK, let selectedFolder = panel.url { - let filePath = selectedFolder.appendingPathComponent("Export-\(appState.appInfo.appName)(v\(appState.appInfo.appVersion)).txt") + +// Print list of files locally +func saveURLsToFile(urls: Set, appState: AppState, copy: Bool = false) { + + if copy { + let homeDirectory = FileManager.default.homeDirectoryForCurrentUser.path var fileContent = "" - var count = 1 let sortedUrls = urls.sorted { $0.path < $1.path } for url in sortedUrls { - fileContent += "[\(count)] - \(url.path)\n" - count += 1 - } - - do { - try fileContent.write(to: filePath, atomically: true, encoding: .utf8) - printOS("File saved successfully at \(filePath.path)") - // Open Finder and select the file - NSWorkspace.shared.selectFile(filePath.path, inFileViewerRootedAtPath: filePath.deletingLastPathComponent().path) - } catch { - printOS("Error saving file: \(error)") + let pathWithTilde = url.path.replacingOccurrences(of: homeDirectory, with: "~") + fileContent += "\(pathWithTilde)\n" } + copyToClipboard(fileContent) } else { - printOS("Folder selection was canceled.") + let panel = NSOpenPanel() + panel.canChooseFiles = false + panel.canChooseDirectories = true + panel.allowsMultipleSelection = false + panel.prompt = "Select Folder" + + if panel.runModal() == .OK, let selectedFolder = panel.url { + let filePath = selectedFolder.appendingPathComponent("Export-\(appState.appInfo.appName)(v\(appState.appInfo.appVersion)).txt") + let homeDirectory = FileManager.default.homeDirectoryForCurrentUser.path + var fileContent = "" + let sortedUrls = urls.sorted { $0.path < $1.path } + + for url in sortedUrls { + let pathWithTilde = url.path.replacingOccurrences(of: homeDirectory, with: "~") + fileContent += "\(pathWithTilde)\n" + } + + do { + try fileContent.write(to: filePath, atomically: true, encoding: .utf8) + printOS("File saved successfully at \(filePath.path)") + // Open Finder and select the file + NSWorkspace.shared.selectFile(filePath.path, inFileViewerRootedAtPath: filePath.deletingLastPathComponent().path) + } catch { + printOS("Error saving file: \(error)") + } + + } else { + printOS("Folder selection was canceled.") + } } + + } diff --git a/Pearcleaner/Settings/General.swift b/Pearcleaner/Settings/General.swift index c3cd6d5..ad31dc7 100644 --- a/Pearcleaner/Settings/General.swift +++ b/Pearcleaner/Settings/General.swift @@ -45,7 +45,7 @@ struct GeneralSettingsTab: View { .foregroundStyle(.primary.opacity(0.5)) } - InfoButton(text: "When homebrew cleanup is enabled, Pearcleaner will check if the app you are removing was installed via homebrew and execute a brew uninstall and brew cleanup command as well to let homebrew know that the app is removed. This way your homebrew list will be synced up correctly and caching will be removed.\n\nNOTE: If you undo the file delete with CMD+Z, the files will be put back but homebrew will not be aware of it. To get the homebrew list back in sync you'd need to run:\n brew install APPNAME --force") + InfoButton(text: "When homebrew cleanup is enabled, Pearcleaner will check if the app you are removing was installed via homebrew and launch Terminal.app to execute a brew uninstall and cleanup command to let homebrew know that the app is removed. This way your homebrew list will be synced up correctly and caching will be removed. Terminal is required since some apps need sudo permissions to remove services and files placed in system folders.\n\nNOTE: If you undo the file delete with CMD+Z, the files will be put back but homebrew will not be aware of it. To get the homebrew list back in sync you'd need to run:\n brew install APPNAME --force") Spacer() Toggle(isOn: $brew, label: {