//go:build darwin package apps import ( "fmt" "os" "os/exec" "path/filepath" "strings" "sync" ) // Structs moved to apps_common.go // ScanApps returns a list of installed applications func ScanApps() ([]AppInfo, error) { // Scan /Applications and ~/Applications home, _ := os.UserHomeDir() dirs := []string{ "/Applications", filepath.Join(home, "Applications"), } var apps []AppInfo var mu sync.Mutex var wg sync.WaitGroup for _, dir := range dirs { entries, err := os.ReadDir(dir) if err != nil { continue } for _, entry := range entries { if strings.HasSuffix(entry.Name(), ".app") { path := filepath.Join(dir, entry.Name()) wg.Add(1) go func(p, name string) { defer wg.Done() // Get Bundle ID bid := getBundleID(p) if bid == "" { return // Skip if no bundle ID (system util or broken) } // Get Size (fast estimate) // using du -s -k size := getDirSize(p) mu.Lock() apps = append(apps, AppInfo{ Name: strings.TrimSuffix(name, ".app"), Path: p, BundleID: bid, Size: size, }) mu.Unlock() }(path, entry.Name()) } } } wg.Wait() return apps, nil } // GetAppDetails finds all associated files for a given app path func GetAppDetails(appPath, _ string) (*AppDetails, error) { bid := getBundleID(appPath) if bid == "" { return nil, fmt.Errorf("could not determine bundle ID") } appSize := getDirSize(appPath) details := &AppDetails{ AppInfo: AppInfo{ Name: filepath.Base(appPath), // simplified Path: appPath, BundleID: bid, Size: appSize, }, TotalSize: appSize, } home, _ := os.UserHomeDir() library := filepath.Join(home, "Library") // Common locations to search for Bundle ID // Name-based search fallback is risky, staying strict to Bundle ID for now locations := map[string]string{ "Application Support": filepath.Join(library, "Application Support"), "Caches": filepath.Join(library, "Caches"), "Preferences": filepath.Join(library, "Preferences"), "Saved Application State": filepath.Join(library, "Saved Application State"), "Logs": filepath.Join(library, "Logs"), "Cookies": filepath.Join(library, "Cookies"), "Containers": filepath.Join(library, "Containers"), // Sandboxed data } for locName, locPath := range locations { // 1. Direct match: path/BundleID target := filepath.Join(locPath, bid) if exists(target) { size := getDirSize(target) details.Associated = append(details.Associated, AssociatedFile{ Path: target, Type: getType(locName), Size: size, }) details.TotalSize += size } // 2. Preferences often use plist extension: path/BundleID.plist if locName == "Preferences" { plistPath := filepath.Join(locPath, bid+".plist") if exists(plistPath) { size := getFileSize(plistPath) details.Associated = append(details.Associated, AssociatedFile{ Path: plistPath, Type: "config", Size: size, }) details.TotalSize += size } } } return details, nil } // DeleteFiles removes the requested paths func DeleteFiles(paths []string) error { for _, p := range paths { // Basic safety check: don't delete root or critical system paths // Real implementation needs robust safeguards if p == "/" || p == "/Applications" || p == "/System" || p == "/Library" { continue // Skip dangerous paths } if err := os.RemoveAll(p); err != nil { return err } } return nil } // Helpers func getBundleID(path string) string { // mdls -name kMDItemCFBundleIdentifier -r /path/to/app cmd := exec.Command("mdls", "-name", "kMDItemCFBundleIdentifier", "-r", path) out, err := cmd.Output() if err != nil { return "" } res := strings.TrimSpace(string(out)) if res == "(null)" { return "" } return res } func getDirSize(path string) int64 { cmd := exec.Command("du", "-s", "-k", path) out, err := cmd.Output() if err != nil { return 0 } parts := strings.Fields(string(out)) if len(parts) > 0 { var s int64 fmt.Sscanf(parts[0], "%d", &s) return s * 1024 } return 0 } func getFileSize(path string) int64 { info, err := os.Stat(path) if err != nil { return 0 } return info.Size() } func exists(path string) bool { _, err := os.Stat(path) return err == nil } func getType(locName string) string { switch locName { case "Caches": return "cache" case "Preferences", "Cookies": return "config" case "Logs": return "log" default: return "data" } } // RunUninstaller executes the uninstall command (Not implemented on Mac yet) func RunUninstaller(cmdString string) error { return fmt.Errorf("uninstall not supported on macOS yet") }