mirror of
https://github.com/exituser/Pearcleaner.git
synced 2026-09-17 04:58:56 +00:00
718 lines
24 KiB
Swift
718 lines
24 KiB
Swift
//
|
|
// Utilities.swift
|
|
// Pearcleaner
|
|
//
|
|
// Created by Alin Lupascu on 11/3/23.
|
|
//
|
|
|
|
import Foundation
|
|
import SwiftUI
|
|
import OSLog
|
|
import CoreImage
|
|
import AppKit
|
|
|
|
// Make updates on main thread
|
|
func updateOnMain(after delay: Double? = nil, _ updates: @escaping () -> Void) {
|
|
if let delay = delay {
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
|
|
updates()
|
|
}
|
|
} else {
|
|
DispatchQueue.main.async {
|
|
updates()
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// Execute functions on background thread
|
|
func updateOnBackground(_ updates: @escaping () -> Void) {
|
|
DispatchQueue.global(qos: .background).async {
|
|
updates()
|
|
}
|
|
}
|
|
|
|
|
|
// Resize window
|
|
//func resizeWindow(width: CGFloat, height: CGFloat) {
|
|
// if let window = NSApplication.shared.windows.first {
|
|
// let newSize = NSSize(width: width, height: height)
|
|
// window.setContentSize(newSize)
|
|
// }
|
|
//}
|
|
|
|
func resizeWindowAuto(windowSettings: WindowSettings, title: String) {
|
|
if let window = NSApplication.shared.windows.first(where: { $0.title == title }) {
|
|
let newSize = NSSize(width: windowSettings.loadWindowSettings().width, height: windowSettings.loadWindowSettings().height)
|
|
window.setContentSize(newSize)
|
|
}
|
|
}
|
|
|
|
|
|
// Check FDA
|
|
func checkFullDiskAccessForApp() -> Bool {
|
|
let process = Process()
|
|
process.launchPath = "/usr/bin/sqlite3"
|
|
process.arguments = ["/Library/Application Support/com.apple.TCC/TCC.db", "select client from access where auth_value and service = \"kTCCServiceSystemPolicyAllFiles\" and client = \"com.alienator88.Pearcleaner\""]
|
|
|
|
let pipe = Pipe()
|
|
let pipeErr = Pipe()
|
|
process.standardOutput = pipe
|
|
process.standardError = pipeErr
|
|
process.launch()
|
|
|
|
let dataErr = pipeErr.fileHandleForReading.readDataToEndOfFile()
|
|
|
|
let output = String(data: dataErr, encoding: .utf8)
|
|
|
|
// Check if the app is in the results
|
|
if let result = output, result.isEmpty {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Check for access to Full Disk access
|
|
func checkAndRequestFullDiskAccess(appState: AppState, skipAlert: Bool = false) -> Bool {
|
|
@AppStorage("settings.permissions.disk") var diskP: Bool = false
|
|
@AppStorage("settings.permissions.hasLaunched") var hasLaunched: Bool = false
|
|
|
|
let process = Process()
|
|
process.launchPath = "/usr/bin/sqlite3"
|
|
process.arguments = ["/Library/Application Support/com.apple.TCC/TCC.db", "select client from access where auth_value and service = \"kTCCServiceSystemPolicyAllFiles\" and client = \"com.alienator88.Pearcleaner\""]
|
|
|
|
let pipe = Pipe()
|
|
let pipeErr = Pipe()
|
|
process.standardOutput = pipe
|
|
process.standardError = pipeErr
|
|
process.launch()
|
|
|
|
let dataErr = pipeErr.fileHandleForReading.readDataToEndOfFile()
|
|
|
|
let output = String(data: dataErr, encoding: .utf8)
|
|
|
|
// Check if the app is in the results
|
|
if let result = output, result.isEmpty {
|
|
diskP = true
|
|
_ = checkAndRequestAccessibilityAccess(appState: appState)
|
|
return true
|
|
} else {
|
|
diskP = false
|
|
if !skipAlert {
|
|
NewWin.show(appState: appState, width: 500, height: 350, newWin: .perm)
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
}
|
|
|
|
|
|
// Check for access to System Events
|
|
func checkAndRequestAccessibilityAccess(appState: AppState) -> Bool {
|
|
@AppStorage("settings.permissions.events") var diskE: Bool = false
|
|
|
|
let options: NSDictionary = [kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: true]
|
|
let accessEnabled = AXIsProcessTrustedWithOptions(options)
|
|
if accessEnabled {
|
|
diskE = true
|
|
return accessEnabled
|
|
} else {
|
|
diskE = false
|
|
return false
|
|
|
|
}
|
|
}
|
|
|
|
// Check app directory based on user permission
|
|
func checkAppDirectoryAndUserRole(completion: @escaping ((isInCorrectDirectory: Bool, isAdmin: Bool)) -> Void) {
|
|
isCurrentUserAdmin { isAdmin in
|
|
let bundlePath = Bundle.main.bundlePath as NSString
|
|
let applicationsDir = "/Applications"
|
|
let userApplicationsDir = "\(home)/Applications"
|
|
|
|
var isInCorrectDirectory = false
|
|
|
|
if isAdmin {
|
|
// Admins can have the app in either /Applications or ~/Applications
|
|
isInCorrectDirectory = bundlePath.deletingLastPathComponent == applicationsDir ||
|
|
bundlePath.deletingLastPathComponent == userApplicationsDir
|
|
} else {
|
|
// Standard users should only have the app in ~/Applications
|
|
isInCorrectDirectory = bundlePath.deletingLastPathComponent == userApplicationsDir
|
|
}
|
|
|
|
// Return both conditions: if the app is in the correct directory and if the user is an admin
|
|
completion((isInCorrectDirectory, isAdmin))
|
|
}
|
|
}
|
|
|
|
|
|
// Check if user is admin or standard user
|
|
func isCurrentUserAdmin(completion: @escaping (Bool) -> Void) {
|
|
let process = Process()
|
|
process.executableURL = URL(fileURLWithPath: "/bin/zsh") // Using zsh, macOS default shell
|
|
process.arguments = ["-c", "groups $(whoami) | grep -q ' admin '"]
|
|
|
|
process.terminationHandler = { process in
|
|
// On macOS, a process's exit status of 0 indicates success (admin group found in this context)
|
|
completion(process.terminationStatus == 0)
|
|
}
|
|
|
|
do {
|
|
try process.run()
|
|
} catch {
|
|
print("Failed to execute command: \(error)")
|
|
completion(false)
|
|
}
|
|
}
|
|
|
|
// Check if appearance is dark mode
|
|
func isDarkModeEnabled() -> Bool {
|
|
let process = Process()
|
|
process.executableURL = URL(fileURLWithPath: "/bin/zsh")
|
|
process.arguments = ["-c", "defaults read -g AppleInterfaceStyle"]
|
|
let pipe = Pipe()
|
|
process.standardOutput = pipe
|
|
process.standardError = pipe
|
|
try? process.run()
|
|
let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
|
if !data.isEmpty{
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
// Check if Pearcleaner has any windows open
|
|
func hasWindowOpen() -> Bool {
|
|
for window in NSApp.windows where window.title == "Pearcleaner" {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Find and hide/show main app window when using menubar item
|
|
func findAndHideWindows(named titles: [String]) {
|
|
for title in titles {
|
|
if let window = NSApp.windows.first(where: { $0.title == title }) {
|
|
// window.orderOut(nil)
|
|
window.close()
|
|
}
|
|
}
|
|
}
|
|
|
|
func findAndShowWindows(named titles: [String]) {
|
|
for title in titles {
|
|
if let window = NSApp.windows.first(where: { $0.title == title }) {
|
|
window.makeKeyAndOrderFront(nil)
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// Check if appearance is dark mode
|
|
//func getCasks() -> [String] {
|
|
// let process = Process()
|
|
//#if arch(x86_64)
|
|
// let cmd = "/usr/local/bin/brew"
|
|
//#elseif arch(arm64)
|
|
// let cmd = "/opt/homebrew/bin/brew"
|
|
//#endif
|
|
// process.executableURL = URL(fileURLWithPath: cmd)
|
|
// process.arguments = ["list", "--cask"]
|
|
// let pipe = Pipe()
|
|
// process.standardOutput = pipe
|
|
// process.standardError = pipe
|
|
// try? process.run()
|
|
// process.waitUntilExit() // Ensure the process completes
|
|
// let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
|
// if let output = String(data: data, encoding: .utf8), !output.isEmpty {
|
|
// return output.components(separatedBy: "\n").filter { !$0.isEmpty }.map { $0.replacingOccurrences(of: "-", with: " ") }
|
|
// } else {
|
|
// return []
|
|
// }
|
|
//}
|
|
|
|
// Brew cleanup
|
|
func caskCleanup(app: String) {
|
|
Task(priority: .high) {
|
|
let process = Process()
|
|
#if arch(x86_64)
|
|
let cmd = "/usr/local/bin/brew"
|
|
#elseif arch(arm64)
|
|
let cmd = "/opt/homebrew/bin/brew"
|
|
#endif
|
|
process.executableURL = URL(fileURLWithPath: "/bin/zsh")
|
|
process.arguments = ["-c", "\(cmd) uninstall --cask \(app) --force; \(cmd) cleanup"]
|
|
// let pipe = Pipe()
|
|
process.standardOutput = FileHandle.nullDevice//pipe
|
|
process.standardError = FileHandle.nullDevice//pipe
|
|
try? process.run()
|
|
process.waitUntilExit() // Ensure the process completes
|
|
// let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
|
// let output = (String(data: data, encoding: .utf8) ?? "") as String
|
|
// print(output)
|
|
}
|
|
}
|
|
|
|
|
|
// Check if symlink
|
|
func isSymlink(atPath path: URL) -> Bool {
|
|
do {
|
|
let _ = try path.checkResourceIsReachable()
|
|
let resourceValues = try path.resourceValues(forKeys: [.isSymbolicLinkKey])
|
|
return resourceValues.isSymbolicLink == true
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
|
|
// Open trash folder
|
|
func openTrash() {
|
|
if let trashURL = try? FileManager.default.url(for: .trashDirectory, in: .userDomainMask, appropriateFor: nil, create: false) {
|
|
NSWorkspace.shared.open(trashURL)
|
|
}
|
|
}
|
|
|
|
// Check if restricted app
|
|
func isRestricted(atPath path: URL) -> Bool {
|
|
if path.path.contains("Safari") || path.path.contains(Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String ?? "") || path.path.contains("/Applications/Utilities") {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
|
|
// Convert icon to png so colors render correctly
|
|
func convertICNSToPNG(icon: NSImage, size: NSSize) -> NSImage? {
|
|
// Resize the icon to the specified size
|
|
let resizedIcon = NSImage(size: size)
|
|
resizedIcon.lockFocus()
|
|
icon.draw(in: NSRect(x: 0, y: 0, width: size.width, height: size.height))
|
|
resizedIcon.unlockFocus()
|
|
|
|
// Convert the resized icon to PNG format
|
|
if let resizedImageData = resizedIcon.tiffRepresentation,
|
|
let resizedBitmap = NSBitmapImageRep(data: resizedImageData),
|
|
let pngData = resizedBitmap.representation(using: .png, properties: [:]) {
|
|
return NSImage(data: pngData)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Get icon for files and folders
|
|
func getIconForFileOrFolder(atPath path: URL) -> Image? {
|
|
let icon = NSWorkspace.shared.icon(forFile: path.path)
|
|
let nsImage = icon
|
|
return Image(nsImage: nsImage)
|
|
}
|
|
|
|
func getIconForFileOrFolderNS(atPath path: URL) -> NSImage? {
|
|
let icon = NSWorkspace.shared.icon(forFile: path.path)
|
|
let nsImage = icon
|
|
return nsImage
|
|
}
|
|
|
|
// Get average color from image
|
|
extension NSImage {
|
|
var averageColor: NSColor? {
|
|
guard let tiffData = self.tiffRepresentation, let bitmapImage = NSBitmapImageRep(data: tiffData), let inputImage = CIImage(bitmapImageRep: bitmapImage) else { return nil }
|
|
|
|
let extentVector = CIVector(x: inputImage.extent.origin.x, y: inputImage.extent.origin.y, z: inputImage.extent.size.width, w: inputImage.extent.size.height)
|
|
|
|
guard let filter = CIFilter(name: "CIAreaAverage", parameters: [kCIInputImageKey: inputImage, kCIInputExtentKey: extentVector]) else { return nil }
|
|
guard let outputImage = filter.outputImage else { return nil }
|
|
|
|
var bitmap = [UInt8](repeating: 0, count: 4)
|
|
let context = CIContext(options: [.workingColorSpace: NSNull()])
|
|
context.render(outputImage, toBitmap: &bitmap, rowBytes: 4, bounds: CGRect(x: 0, y: 0, width: 1, height: 1), format: CIFormat.RGBA8, colorSpace: nil)
|
|
|
|
return NSColor(red: CGFloat(bitmap[0]) / 255, green: CGFloat(bitmap[1]) / 255, blue: CGFloat(bitmap[2]) / 255, alpha: CGFloat(bitmap[3]) / 255)
|
|
}
|
|
}
|
|
|
|
func startEnd(_ function: @escaping () -> Void) {
|
|
let startTime = Date() // Capture start time
|
|
function()
|
|
let endTime = Date()
|
|
let executionTime = endTime.timeIntervalSince(startTime)
|
|
printOS("Function executed in: \n\(executionTime) seconds")
|
|
}
|
|
|
|
|
|
// Relaunch app
|
|
func relaunchApp(afterDelay seconds: TimeInterval = 0.5) -> Never {
|
|
let task = Process()
|
|
task.launchPath = "/bin/sh"
|
|
task.arguments = ["-c", "sleep \(seconds); open \"\(Bundle.main.bundlePath)\""]
|
|
task.launch()
|
|
|
|
NSApp.terminate(nil)
|
|
exit(0)
|
|
}
|
|
|
|
// Check if app is running before deleting app files
|
|
func killApp(appId: String, completion: @escaping () -> Void = {}) {
|
|
let runningApps = NSWorkspace.shared.runningApplications
|
|
for app in runningApps {
|
|
if app.bundleIdentifier == appId {
|
|
app.terminate()
|
|
}
|
|
}
|
|
completion()
|
|
}
|
|
|
|
// Remove app from cache
|
|
func removeApp(appState: AppState, withId id: UUID) {
|
|
DispatchQueue.main.async {
|
|
// Remove from sortedApps if found
|
|
if let index = appState.sortedApps.firstIndex(where: { $0.id == id }) {
|
|
appState.sortedApps.remove(at: index)
|
|
return // Exit the function if the app was found and removed
|
|
}
|
|
// Remove from appInfoStore if found
|
|
if let index = appState.appInfoStore.firstIndex(where: { $0.id == id }) {
|
|
appState.appInfoStore.remove(at: index)
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// Check if app bundle is nested
|
|
func isNested(path: URL) -> Bool {
|
|
let applicationsPath = "/Applications"
|
|
let homeApplicationsPath = "\(home)/Applications"
|
|
|
|
guard path.path.contains("Applications") else {
|
|
return false
|
|
}
|
|
|
|
// Get the parent directory of the app
|
|
let parentDirectory = path.deletingLastPathComponent().path
|
|
|
|
// Check if the parent directory is not directly /Applications or ~/Applications
|
|
return parentDirectory != applicationsPath && parentDirectory != homeApplicationsPath
|
|
}
|
|
|
|
|
|
|
|
// --- Extend Int to convert hours to seconds ---
|
|
extension Int {
|
|
var daysToSeconds: Double {
|
|
return Double(self) * 24 * 60 * 60
|
|
}
|
|
}
|
|
|
|
|
|
// --- Extend String to remove periods, spaces and lowercase the string
|
|
extension String {
|
|
func pearFormat() -> String {
|
|
return self.replacingOccurrences(of: ".", with: "").replacingOccurrences(of: " ", with: "").replacingOccurrences(of: "-", with: "").lowercased()
|
|
}
|
|
}
|
|
|
|
// --- Extend string to replace - and | with custom characters
|
|
extension String {
|
|
func featureFormat() -> String {
|
|
return self.replacingOccurrences(of: "- ", with: "• ").replacingOccurrences(of: "|", with: "\n\n")
|
|
}
|
|
}
|
|
|
|
// --- Capitalize first letter of string only
|
|
extension String {
|
|
func capitalizingFirstLetter() -> String {
|
|
return prefix(1).capitalized + dropFirst()
|
|
}
|
|
|
|
mutating func capitalizeFirstLetter() {
|
|
self = self.capitalizingFirstLetter()
|
|
}
|
|
}
|
|
|
|
|
|
// --- 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
|
|
}
|
|
}
|
|
|
|
// --- 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: "pearcleaner", category: "Application")
|
|
os_log("%@", log: log, type: .default, message)
|
|
|
|
}
|
|
|
|
|
|
|
|
//func totalSizeOnDisk2(for paths: [URL]) -> Int64 {
|
|
// var totalSize = 0
|
|
//
|
|
// let process = Process()
|
|
// process.launchPath = "/usr/bin/du"
|
|
// process.arguments = ["-sk"] + paths.map { (url: URL) -> String in
|
|
// return url.path
|
|
// }
|
|
// let pipe = Pipe()
|
|
// process.standardOutput = pipe
|
|
// process.standardError = pipe//FileHandle.nullDevice
|
|
//
|
|
// try? process.run()
|
|
// process.waitUntilExit()
|
|
//
|
|
// let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
|
// if let output = String(data: data, encoding: .utf8) {
|
|
// let lines = output.components(separatedBy: .newlines)
|
|
// for line in lines {
|
|
// let components = line.components(separatedBy: "\t")
|
|
// if let sizeString = components.first, let size = Int(sizeString) {
|
|
// totalSize += size
|
|
// }
|
|
// }
|
|
// }
|
|
//
|
|
// return Int64(totalSize) * 1024 // Convert the size from kilobytes to bytes
|
|
//}
|
|
|
|
|
|
// Get total size of folders and files using DU cli command
|
|
func totalSizeOnDisk(for paths: [URL]) -> Int64 {
|
|
let fileManager = FileManager.default
|
|
var totalSize: Int64 = 0
|
|
|
|
for url in paths {
|
|
var isDirectory: ObjCBool = false
|
|
if fileManager.fileExists(atPath: url.path, isDirectory: &isDirectory) {
|
|
if isDirectory.boolValue {
|
|
// It's a directory, recurse into it
|
|
if let enumerator = fileManager.enumerator(at: url, includingPropertiesForKeys: [.totalFileAllocatedSizeKey], errorHandler: nil) {
|
|
for case let fileURL as URL in enumerator {
|
|
do {
|
|
let fileAttributes = try fileURL.resourceValues(forKeys: [.totalFileAllocatedSizeKey])
|
|
if let fileSize = fileAttributes.totalFileAllocatedSize {
|
|
totalSize += Int64(fileSize)
|
|
}
|
|
} catch {
|
|
printOS("Error getting file size for \(fileURL): \(error)")
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// It's a file
|
|
do {
|
|
let fileAttributes = try url.resourceValues(forKeys: [.totalFileAllocatedSizeKey])
|
|
if let fileSize = fileAttributes.totalFileAllocatedSize {
|
|
totalSize += Int64(fileSize)
|
|
}
|
|
} catch {
|
|
printOS("Error getting file size for \(url): \(error)")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return totalSize
|
|
}
|
|
|
|
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: size)
|
|
}
|
|
|
|
// Only process supported files
|
|
func isSupportedFileType(at path: String) -> Bool {
|
|
let fileManager = FileManager.default
|
|
do {
|
|
let attributes = try fileManager.attributesOfItem(atPath: path)
|
|
if let fileType = attributes[FileAttributeKey.type] as? FileAttributeType {
|
|
switch fileType {
|
|
case .typeRegular, .typeDirectory, .typeSymbolicLink:
|
|
// The file is a regular file, directory, or symbolic link
|
|
return true
|
|
default:
|
|
// The file is a socket, pipe, or another type not supported
|
|
return false
|
|
}
|
|
}
|
|
} catch {
|
|
printOS("Error getting file attributes: \(error)")
|
|
}
|
|
return false
|
|
}
|
|
|
|
|
|
// 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 --
|
|
func uninstallPearcleaner(appState: AppState, locations: Locations) {
|
|
|
|
// Unload Sentinel Monitor if running
|
|
launchctl(load: false)
|
|
|
|
// Get app info for Pearcleaner
|
|
let appInfo = AppInfoFetcher.getAppInfo(atPath: Bundle.main.bundleURL)
|
|
// appState.appInfo = appInfo!
|
|
|
|
// Find application files for Pearcleaner
|
|
let pathFinder = AppPathFinder(appInfo: appInfo!, appState: appState, locations: locations)
|
|
pathFinder.findPaths()
|
|
// findPathsForApp(appInfo: appInfo!,appState: appState, locations: locations)
|
|
|
|
// Kill Pearcleaner and tell Finder to trash the files
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
|
|
let selectedItemsArray = Array(appState.selectedItems).filter { !$0.path.contains(".Trash") }
|
|
let posixFiles = selectedItemsArray.map { "POSIX file \"\($0.path)\", " }.joined().dropLast(3)
|
|
let scriptSource = """
|
|
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)
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// --- Create Application Support folder if it doesn't exist ---
|
|
func ensureApplicationSupportFolderExists(appState: AppState) {
|
|
let fileManager = FileManager.default
|
|
let supportURL = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!.appendingPathComponent("Pearcleaner")
|
|
|
|
// Check to make sure Application Support/Pearcleaner folder exists
|
|
if !fileManager.fileExists(atPath: supportURL.path) {
|
|
try! fileManager.createDirectory(at: supportURL, withIntermediateDirectories: true)
|
|
printOS("Created Application Support/Pearcleaner folder")
|
|
}
|
|
}
|
|
|
|
|
|
// --- Write Log to File for troubleshooting ---
|
|
func writeLog(string: String) {
|
|
let fileManager = FileManager.default
|
|
let home = fileManager.homeDirectoryForCurrentUser.path
|
|
let logFilePath = "\(home)/Downloads/log.txt"
|
|
|
|
// 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) {
|
|
printOS("Failed to create the log file.")
|
|
return
|
|
}
|
|
}
|
|
|
|
do {
|
|
if let fileHandle = FileHandle(forWritingAtPath: logFilePath) {
|
|
let ns = "\(string)\n"
|
|
fileHandle.seekToEndOfFile()
|
|
fileHandle.write(ns.data(using: .utf8)!)
|
|
fileHandle.closeFile()
|
|
} else {
|
|
printOS("Error opening file for appending")
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// --- Load Plist file with launchctl ---
|
|
func launchctl(load: Bool, completion: @escaping () -> Void = {}) {
|
|
let cmd = load ? "load" : "unload"
|
|
|
|
if let plistPath = Bundle.main.path(forResource: "com.alienator88.PearcleanerSentinel", ofType: "plist") {
|
|
var plistContent = try! String(contentsOfFile: plistPath)
|
|
let executableURL = Bundle.main.bundleURL.appendingPathComponent("Contents/MacOS/PearcleanerSentinel")
|
|
|
|
// 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.PearcleanerSentinel.plist")
|
|
|
|
do {
|
|
try plistContent.write(to: temporaryPlistURL, atomically: true, encoding: .utf8)
|
|
} catch {
|
|
printOS("Error writing the temporary plist file: \(error)")
|
|
return
|
|
}
|
|
|
|
let task = Process()
|
|
task.launchPath = "/bin/launchctl"
|
|
task.arguments = [cmd, "-w", temporaryPlistURL.path]
|
|
task.standardOutput = FileHandle.nullDevice
|
|
task.standardError = FileHandle.nullDevice
|
|
task.launch()
|
|
|
|
completion()
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func getCurrentTimestamp() -> String {
|
|
let dateFormatter = DateFormatter()
|
|
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS"
|
|
return dateFormatter.string(from: Date())
|
|
}
|
|
|
|
|
|
|
|
|