//go:build darwin package scanner import ( "fmt" "os" "os/exec" "path/filepath" "sort" "strconv" "strings" ) // Structs moved to scanner_common.go // GetDiskUsage uses diskutil for accurate APFS disk usage func GetDiskUsage() ([]*DiskUsage, error) { cmd := exec.Command("diskutil", "info", "/") out, err := cmd.Output() if err != nil { return nil, err } lines := strings.Split(string(out), "\n") var containerTotal, containerFree int64 for _, line := range lines { line = strings.TrimSpace(line) // Parse "Container Total Space:" line if strings.HasPrefix(line, "Container Total Space:") { // Format: "Container Total Space: 245.1 GB (245107195904 Bytes)" if start := strings.Index(line, "("); start != -1 { if end := strings.Index(line[start:], " Bytes)"); end != -1 { bytesStr := line[start+1 : start+end] containerTotal, _ = strconv.ParseInt(bytesStr, 10, 64) } } } // Parse "Container Free Space:" line if strings.HasPrefix(line, "Container Free Space:") { if start := strings.Index(line, "("); start != -1 { if end := strings.Index(line[start:], " Bytes)"); end != -1 { bytesStr := line[start+1 : start+end] containerFree, _ = strconv.ParseInt(bytesStr, 10, 64) } } } } // Calculate used space containerUsed := containerTotal - containerFree toGB := func(bytes int64) string { gb := float64(bytes) / 1024 / 1024 / 1024 return fmt.Sprintf("%.2f", gb) } return []*DiskUsage{{ Name: "Macintosh HD", TotalGB: toGB(containerTotal), UsedGB: toGB(containerUsed), FreeGB: toGB(containerFree), }}, nil } // FindLargeFiles moved to scanner_common.go // FindHeavyFolders uses `du` to find large directories func FindHeavyFolders(root string) ([]ScanResult, error) { // du -k -d 2 | sort -nr | head -n 50 cmd := exec.Command("bash", "-c", fmt.Sprintf("du -k -d 2 \"%s\" | sort -nr | head -n 50", root)) out, err := cmd.Output() if err != nil { return nil, err } var results []ScanResult lines := strings.Split(string(out), "\n") for _, line := range lines { line = strings.TrimSpace(line) if line == "" { continue } // Attempt to parse first part as size firstSpace := strings.IndexAny(line, " \t") if firstSpace == -1 { continue } sizeStr := line[:firstSpace] pathStr := strings.TrimSpace(line[firstSpace:]) if pathStr == root { continue } sizeK, err := strconv.ParseInt(sizeStr, 10, 64) if err == nil { results = append(results, ScanResult{ Path: pathStr, Size: sizeK * 1024, IsDirectory: true, }) } } return results, nil } func ScanUserDocuments() ([]ScanResult, error) { home, err := os.UserHomeDir() if err != nil { return nil, err } targets := []string{ filepath.Join(home, "Documents"), filepath.Join(home, "Downloads"), filepath.Join(home, "Desktop"), } var allResults []ScanResult for _, t := range targets { // 10MB threshold res, _ := FindLargeFiles(t, 10*1024*1024) allResults = append(allResults, res...) } // Sort combined sort.Slice(allResults, func(i, j int) bool { return allResults[i].Size > allResults[j].Size }) if len(allResults) > 50 { return allResults[:50], nil } return allResults, nil } func ScanSystemData() ([]ScanResult, error) { home, err := os.UserHomeDir() if err != nil { return nil, err } // System targets: Caches, Logs, Application Support (selective) targets := []string{ filepath.Join(home, "Library", "Caches"), filepath.Join(home, "Library", "Logs"), filepath.Join(home, "Library", "Developer", "Xcode", "DerivedData"), } var allResults []ScanResult for _, t := range targets { // 10MB threshold res, _ := FindLargeFiles(t, 10*1024*1024) allResults = append(allResults, res...) } sort.Slice(allResults, func(i, j int) bool { return allResults[i].Size > allResults[j].Size }) if len(allResults) > 50 { return allResults[:50], nil } return allResults, nil } func GetDirectorySize(path string) int64 { // du -s -k cmd := exec.Command("du", "-s", "-k", path) out, err := cmd.Output() if err != nil { return 0 } // Output is "size path" parts := strings.Fields(string(out)) if len(parts) > 0 { sizeK, _ := strconv.ParseInt(parts[0], 10, 64) return sizeK * 1024 // Bytes } return 0 } func GetCategorySizes() (*CategorySizes, error) { home, err := os.UserHomeDir() if err != nil { return nil, err } // Paths to check docPath := filepath.Join(home, "Documents") downPath := filepath.Join(home, "Downloads") deskPath := filepath.Join(home, "Desktop") musicPath := filepath.Join(home, "Music") moviesPath := filepath.Join(home, "Movies") caches := filepath.Join(home, "Library", "Caches") logs := filepath.Join(home, "Library", "Logs") xcode := filepath.Join(home, "Library", "Developer", "Xcode", "DerivedData") trash := filepath.Join(home, ".Trash") apps := "/Applications" photos := filepath.Join(home, "Pictures") icloud := filepath.Join(home, "Library", "Mobile Documents") // Run in parallel type result struct { name string size int64 } // Increased buffer for new categories c := make(chan result) totalChecks := 12 check := func(name, p string) { c <- result{name, GetDirectorySize(p)} } go check("docs", docPath) go check("down", downPath) go check("desk", deskPath) go check("music", musicPath) go check("movies", moviesPath) go check("caches", caches) go check("logs", logs) go check("xcode", xcode) go check("trash", trash) go check("apps", apps) go check("photos", photos) go check("icloud", icloud) sizes := &CategorySizes{} // Get total disk usage to calculate System Data remainder using diskutil (APFS aware) cmd := exec.Command("diskutil", "info", "/") out, err := cmd.Output() var totalUsed int64 if err == nil { lines := strings.Split(string(out), "\n") var containerTotal, containerFree int64 for _, line := range lines { line = strings.TrimSpace(line) if strings.HasPrefix(line, "Container Total Space:") { if start := strings.Index(line, "("); start != -1 { if end := strings.Index(line[start:], " Bytes)"); end != -1 { bytesStr := line[start+1 : start+end] containerTotal, _ = strconv.ParseInt(bytesStr, 10, 64) } } } if strings.HasPrefix(line, "Container Free Space:") { if start := strings.Index(line, "("); start != -1 { if end := strings.Index(line[start:], " Bytes)"); end != -1 { bytesStr := line[start+1 : start+end] containerFree, _ = strconv.ParseInt(bytesStr, 10, 64) } } } } totalUsed = containerTotal - containerFree // In Bytes } var systemSpecific int64 for i := 0; i < totalChecks; i++ { res := <-c switch res.name { case "docs": sizes.Documents = res.size case "down": sizes.Downloads = res.size case "desk": sizes.Desktop = res.size case "music": sizes.Music = res.size case "movies": sizes.Movies = res.size case "caches", "logs", "xcode": systemSpecific += res.size case "trash": sizes.Trash += res.size case "apps": sizes.Apps = res.size case "photos": sizes.Photos = res.size case "icloud": sizes.ICloud = res.size } } // Calculate System Data // Ideally: System = Total - (Docs + Down + Desk + Music + Movies + Apps + Photos + iCloud + Trash) known := sizes.Documents + sizes.Downloads + sizes.Desktop + sizes.Music + sizes.Movies + sizes.Trash + sizes.Apps + sizes.Photos + sizes.ICloud var remainder int64 = 0 if totalUsed > known { remainder = totalUsed - known } if remainder > systemSpecific { sizes.System = remainder } else { sizes.System = systemSpecific } return sizes, nil } // CleaningEstimates struct moved to scanner_common.go func GetCleaningEstimates() (*CleaningEstimates, error) { home, err := os.UserHomeDir() if err != nil { return nil, err } // Paths for Flash Clean: Caches, Logs, Trash, Xcode caches := filepath.Join(home, "Library", "Caches") logs := filepath.Join(home, "Library", "Logs") trash := filepath.Join(home, ".Trash") xcode := filepath.Join(home, "Library", "Developer", "Xcode", "DerivedData") // Paths for Deep Clean (proxy): Downloads downloads := filepath.Join(home, "Downloads") type result struct { name string size int64 } c := make(chan result) totalChecks := 5 check := func(name, p string) { c <- result{name, GetDirectorySize(p)} } go check("caches", caches) go check("logs", logs) go check("trash", trash) go check("xcode", xcode) go check("downloads", downloads) estimates := &CleaningEstimates{} for i := 0; i < totalChecks; i++ { res := <-c switch res.name { case "caches", "logs", "trash", "xcode": estimates.FlashEst += res.size case "downloads": estimates.DeepEst += res.size } } return estimates, nil }