Last active
February 3, 2026 07:09
-
-
Save david-saint/6e62006d9ec6f4aea11e64b23dcbcdb5 to your computer and use it in GitHub Desktop.
macOS Application and Support Storage Usage Calculator
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package main | |
| import ( | |
| "fmt" | |
| "io/fs" | |
| "os" | |
| "path/filepath" | |
| "sort" | |
| "strings" | |
| "sync" | |
| "time" | |
| ) | |
| type ItemInfo struct { | |
| Path string | |
| Size int64 | |
| } | |
| func main() { | |
| home := os.Getenv("HOME") | |
| // Strategy 1: Find .app bundles recursively | |
| appDirs := []string{ | |
| "/Applications", | |
| filepath.Join(home, "Applications"), | |
| } | |
| // Strategy 2: Aggregate immediate subdirectories | |
| supportDirs := []string{ | |
| filepath.Join(home, "Library", "Application Support"), | |
| } | |
| fmt.Println("Calculating storage usage... This might take a moment.") | |
| start := time.Now() | |
| var items []ItemInfo | |
| var totalSize int64 | |
| var mu sync.Mutex | |
| var wg sync.WaitGroup | |
| // 1. Scan Applications folders | |
| for _, dir := range appDirs { | |
| wg.Add(1) | |
| go func(root string) { | |
| defer wg.Done() | |
| if _, err := os.Stat(root); os.IsNotExist(err) { | |
| return | |
| } | |
| // Walk looking for .app | |
| filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error { | |
| if err != nil { | |
| return nil | |
| } | |
| if d.IsDir() && strings.HasSuffix(d.Name(), ".app") { | |
| size, err := getDirSize(path) | |
| if err != nil { | |
| return nil | |
| } | |
| mu.Lock() | |
| items = append(items, ItemInfo{Path: path, Size: size}) | |
| totalSize += size | |
| mu.Unlock() | |
| return filepath.SkipDir // Don't look inside the app bundle | |
| } | |
| return nil | |
| }) | |
| }(dir) | |
| } | |
| // 2. Scan Support folders (Top-level aggregation) | |
| for _, dir := range supportDirs { | |
| wg.Add(1) | |
| go func(root string) { | |
| defer wg.Done() | |
| entries, err := os.ReadDir(root) | |
| if err != nil { | |
| return | |
| } | |
| for _, entry := range entries { | |
| if entry.Name() == ".DS_Store" { | |
| continue | |
| } | |
| fullPath := filepath.Join(root, entry.Name()) | |
| var size int64 | |
| if entry.IsDir() { | |
| size, _ = getDirSize(fullPath) | |
| } else { | |
| info, err := entry.Info() | |
| if err == nil { | |
| size = info.Size() | |
| } | |
| } | |
| if size > 0 { | |
| mu.Lock() | |
| items = append(items, ItemInfo{Path: fullPath, Size: size}) | |
| totalSize += size | |
| mu.Unlock() | |
| } | |
| } | |
| }(dir) | |
| } | |
| wg.Wait() | |
| // Sort by size (descending) | |
| sort.Slice(items, func(i, j int) bool { | |
| return items[i].Size > items[j].Size | |
| }) | |
| fmt.Printf("\nFound %d items in %v\n", len(items), time.Since(start).Round(time.Millisecond)) | |
| fmt.Println("--------------------------------------------------------------------------------") | |
| // Print top 25 | |
| limit := 25 | |
| if len(items) < limit { | |
| limit = len(items) | |
| } | |
| for i := 0; i < limit; i++ { | |
| fmt.Printf("% -10s %s\n", formatBytes(items[i].Size), items[i].Path) | |
| } | |
| if len(items) > limit { | |
| fmt.Printf("... and %d more ...\n", len(items)-limit) | |
| } | |
| fmt.Println("--------------------------------------------------------------------------------") | |
| fmt.Printf("Total Scanned Size: %s\n", formatBytes(totalSize)) | |
| } | |
| func getDirSize(path string) (int64, error) { | |
| var size int64 | |
| err := filepath.WalkDir(path, func(_ string, d fs.DirEntry, err error) error { | |
| if err != nil { | |
| return nil | |
| } | |
| if !d.IsDir() { | |
| info, err := d.Info() | |
| if err == nil { | |
| size += info.Size() | |
| } | |
| } | |
| return nil | |
| }) | |
| return size, err | |
| } | |
| func formatBytes(b int64) string { | |
| const unit = 1000 | |
| if b < unit { | |
| return fmt.Sprintf("%d B", b) | |
| } | |
| div, exp := int64(unit), 0 | |
| for n := b / unit; n >= unit; n /= unit { | |
| div *= unit | |
| exp++ | |
| } | |
| return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "kMGTPE"[exp]) | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment