Skip to content

About

A fast, modern macOS app for scanning, analyzing, and cleaning storage — built for developers and power users.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

17 Commits

Folders and files

Repository files navigation

MacHarmony

The macOS app for MacHarmony — an open-source storage scanner and cleaner.

MacHarmony Hero

A fast, modern macOS app for scanning, analyzing, and cleaning storage — built for developers and power users.


Install

Build an optimized Release copy from source and install it to /Applications like any other app — no Xcode window, no Debug build.

Prerequisites: macOS 14 (Sonoma) or later, and Xcode 26+ (the full app — the Command Line Tools alone are not enough). Open Xcode once after installing it so it can finish setup.

One command

git clone https://github.com/saleh2001k/MacHarmony.git && cd MacHarmony && ./scripts/install.sh

Already cloned? From the repo root:

./scripts/install.sh

When it finishes, MacHarmony is in /Applications and shows up in Launchpad and Spotlight.

What the script does

  1. Builds the MacHarmony scheme in the Release configuration (universal: Apple silicon + Intel) into build/install/
  2. Signs it ad-hoc ("Sign to Run Locally") — no Apple Developer account needed
  3. Quits MacHarmony if it is running, then replaces /Applications/MacHarmony.app (asks for your password only if that folder isn't writable)
  4. Verifies the code signature and launches the app

Options

Option Effect
--no-launch Install without opening the app
INSTALL_DIR="$HOME/Applications" Install somewhere other than /Applications
TEAM_ID=ABCDE12345 Sign with your Apple Developer team instead of ad-hoc (see below)

Example: TEAM_ID=ABCDE12345 ./scripts/install.sh --no-launch

Update / uninstall

  • Update: git pull && ./scripts/install.sh
  • Uninstall: ./scripts/uninstall.sh — removes the app plus its caches, preferences and containers (uses sudo)

Permissions after reinstalling

macOS ties privacy permissions such as Full Disk Access to the app's code signature. An ad-hoc build gets a new signature every time it is rebuilt, so after an update you may need to re-enable MacHarmony in System Settings → Privacy & Security → Full Disk Access (remove it with –, then add /Applications/MacHarmony.app again with +).

To keep permissions across updates, sign with your own team: add your Apple ID in Xcode → Settings → Accounts, then pass your TEAM_ID (shown at developer.apple.com/account → Membership details).

Prefer Xcode?

  1. Open MacHarmony.xcodeproj, select the MacHarmony target → Signing & Capabilities, and pick your Team (or set Signing Certificate to Sign to Run Locally)
  2. Product → Archive
  3. In the Organizer: Distribute App → Custom → Copy App, then drag the exported MacHarmony.app into /Applications

Features

  • Parallel scanning — all categories scan concurrently using Swift structured concurrency; results stream in as each finishes
  • Shared file index — one fts_open traversal builds an in-memory index upfront; all deepFiles and deepLargeFiles categories filter it in O(n) instead of re-traversing disk
  • Disk cache — scan results are persisted to JSON with a configurable TTL (default 1h); reopening the app loads instantly
  • Incremental deletion — items disappear from the list one-by-one as each file is deleted; file I/O runs on a background thread so the UI never freezes
  • Deletion progress HUD — a floating pill at the bottom shows current file name, batch count, and a linear progress bar
  • Custom scanners — define your own categories by extension + directory
  • Category ordering — drag-and-drop reorder in Settings; persisted across launches
  • Ignored patterns — folder names to skip in deep scans (e.g. .git, build, dist)
  • Excluded paths — explicit directories to skip entirely during all scans
  • Menu bar access — quick stats and scan trigger from the menu bar
  • Quick Look — press Space on a selected item in category detail to preview it
  • Export — save scan results as CSV, JSON, or plain text
  • Settings — 6-tab settings window (General, Scan, Categories, Notifications, Export, Custom Scanners)
  • Tools (sidebar) — twelve standalone utilities (see Tools below); routed by ToolRouterView in ContentView.swift
  • Loading indicators — indeterminate spinners use pure SwiftUI (IndeterminateSpinner in ViewsToolSharedComponents.swift), not AppKit NSProgressIndicator, to avoid layout console noise

Tools (sidebar)

The sidebar lists Tools in ToolType / ToolType.allCases order. Each row opens a dedicated view via ToolRouterView.

Tool View file What it does
Large File Browser ViewsLargeFileBrowserView.swift Files larger than a user-chosen size threshold; grouped by type
Old & Unused Files ViewsOldUnusedFilesView.swift Files not modified in 6, 12, or 24 months (segmented control); uses file index after a full scan or merged cached paths
Empty Folders ViewsEmptyFolderView.swift Finds empty directories under the home folder
App Uninstaller ViewsAppUninstallerView.swift Lists apps; drives uninstall / support-file cleanup
App Cache Breakdown ViewsAppCacheView.swift Per-app cache size breakdown
Leftover Files ViewsLeftoverFilesView.swift Orphaned Library folders after app removal (pairs with uninstaller scan)
Startup Items ViewsStartupItemsView.swift Login items and launch agents
Memory Monitor ViewsMemoryMonitorView.swift RAM usage and “free inactive memory”
Disk Health ViewsDiskHealthView.swift Volume / SMART-style disk info
Scan History ViewsScanHistoryView.swift History of space freed over time
Safe Mode Queue ViewsSafeModeView.swift Stage items before committing deletion
iCloud Cleaner ViewsiCloudCleanerView.swift iCloud-backed files reclaim hints

Supporting types: ModelsToolType.swift (enum + LargeFileGroup), ModelsAppEntry.swift (app entries for uninstaller/cache tools). Tool-specific services include ServicesAppScanner, ServicesEmptyFolderScanner, ServicesLeftoverScanner, ServicesStartupItemScanner, ServicesDiskHealth, ServicesDiskUsage, ServicesMemoryMonitor, ServicesScanHistory.


Project Structure

Flat layout under MacHarmony/ (Xcode may group files visually; names match the repo).

MacHarmony/
│
├── MacHarmonyApp.swift              # App entry, scenes, menu bar, Settings
├── ContentView.swift                   # NavigationSplitView, toolbar, ToolRouterView
│
├── Models/
│   ├── ModelsStorageCategory.swift     # StorageCategory + ScanStyle
│   ├── ModelsStorageItem.swift         # StorageItem (Codable)
│   ├── ModelsCustomCategory.swift      # CustomScanCategory
│   ├── ModelsToolType.swift            # ToolType enum + LargeFileGroup
│   └── ModelsAppEntry.swift            # App bundle entries (uninstaller / cache tools)
│
├── ViewModels/
│   └── ViewModelsStorageManager.swift  # StorageManager — state, scan, delete, tools, cache
│
├── Services/
│   ├── ServicesStorageScanner.swift    # Actor — category scans, deep scans, custom scans
│   ├── ServicesFastScanEngine.swift    # fts-based traversal (async streams)
│   ├── ServicesFileIndex.swift         # Shared in-memory index for one pass per scan
│   ├── ServicesSizeCalculator.swift    # du -sk sizing
│   ├── ServicesScanCache.swift          # On-disk size cache (paths + TTL)
│   ├── ServicesResultsCache.swift      # Full scan results JSON for relaunch
│   ├── ServicesThumbnailCache.swift    # Thumbnail NSCache
│   ├── ServicesCustomCategoryStore.swift
│   ├── ServicesReportExporter.swift
│   ├── ServicesFileAccessService.swift # Home folder security-scoped access
│   ├── ServicesQuickLookCoordinator.swift
│   ├── ServicesDiskUsage.swift         # Tool: volume usage
│   ├── ServicesDiskHealth.swift
│   ├── ServicesMemoryMonitor.swift
│   ├── ServicesScanHistory.swift
│   ├── ServicesAppScanner.swift
│   ├── ServicesEmptyFolderScanner.swift
│   ├── ServicesLeftoverScanner.swift
│   └── ServicesStartupItemScanner.swift
│
├── Views/  (main UI + tools)
│   ├── ViewsSidebarView.swift          # Storage categories, Tools, custom scanners
│   ├── ViewsCategoryDetailView.swift
│   ├── ViewsCustomCategoryDetailView.swift
│   ├── ViewsStorageOverviewView.swift
│   ├── ViewsDashboardView.swift
│   ├── ViewsScanningView.swift         # ScanProgressBanner, ScanningFullView, category chips
│   ├── ViewsSettingsView.swift
│   ├── ViewsMenuBarQuickAccessView.swift
│   ├── ViewsAddCustomCategoryView.swift
│   ├── ViewsHelpView.swift
│   ├── ViewsTheme.swift
│   ├── ViewsToolSharedComponents.swift # IndeterminateSpinner, LinearProgressFill, ToolLoadingView, …
│   │
│   ├── ViewsLargeFileBrowserView.swift
│   ├── ViewsOldUnusedFilesView.swift
│   ├── ViewsEmptyFolderView.swift
│   ├── ViewsAppUninstallerView.swift
│   ├── ViewsAppCacheView.swift
│   ├── ViewsLeftoverFilesView.swift
│   ├── ViewsStartupItemsView.swift
│   ├── ViewsMemoryMonitorView.swift
│   ├── ViewsDiskHealthView.swift
│   ├── ViewsScanHistoryView.swift
│   ├── ViewsSafeModeView.swift
│   └── ViewsiCloudCleanerView.swift
│
└── (Assets & entitlements in Xcode project)

Architecture

MVVM + Actor Services

View  ──────────────────────►  StorageManager (@MainActor ObservableObject)
                                      │
                   ┌──────────────────┤
                   │                  │
             StorageScanner     ResultsCache
               (actor)            (actor)
                   │
        ┌──────────┼──────────┐
        │          │          │
  FastScanEngine FileIndex SizeCalculator
  (async stream) (in-memory) (du subprocess)
        │
    ScanCache
  (JSON on disk)
  • **StorageManager** is the single source of truth. All @Published state lives here; Views bind to it directly.
  • **StorageScanner** is an actor. StorageManager.scan() calls it from the main actor; the actor runs all work off-thread.
  • **FileIndex** is built once per scan (one parallel FTS traversal). Categories that use deepFiles or deepLargeFiles filter the index in memory instead of re-traversing disk — dramatically faster for repeated scans.
  • **ResultsCache** persists the full [String: [StorageItem]] dictionary to a JSON file. On relaunch, if the cache is within the TTL, results load in milliseconds.

Scanning Pipeline

StorageManager.scan()
  │
  ├─ scanner.configure(ignoredNames:excludedPaths:globalMinBytes:)
  │
  ├─ scanner.buildIndex(rootPath:)       ← one fts_open pass, builds FileIndex
  │
  └─ withTaskGroup { for each category }
        │
        └─ scanner.scanCategory(_:rootPath:index:)
              │
              ├─ .nodeModules    → scanNodeModules       ← FTS name-match + du -sk
              ├─ .topLevelDirs   → scanTopLevelDirs      ← shallow dirs + du -sk, size threshold
              ├─ .topLevelAll    → scanTopLevelAll        ← shallow files+dirs, size from fts stat
              ├─ .deepFiles      → index.filter(ext:)    ← O(n) in-memory (or scanFilesDeep)
              ├─ .deepLargeFiles → index.filter(min:)    ← O(n) in-memory (or scanFilesDeep)
              └─ .timeMachine    → tmutil listlocalsnapshots

  scanFilesDeep (fallback when index misses)
        ├─ expands root into parallel sub-dirs
        ├─ filters: ignoredNames, excludedPaths, extensions, minBytes
        └─ fts stat for file size (no subprocess)

Deletion Flow

User selects → confirmation → deleteItems(_:from:mode:)
  │
  └─ performDeletion(_:mode:onItemDeleted:)
        │
        for each item:
          ├─ Task.detached { FileManager.trashItem / removeItem }  ← background thread
          ├─ await result                                           ← main actor suspended (UI responsive)
          ├─ onItemDeleted(item) → storageItems[category].remove   ← item vanishes from list live
          └─ publish: deletionCurrentName, deletionCompleted, deletionProgress
        │
        ├─ scanner.evictCache(for: deleted)
        └─ resultsCache.invalidate()

Categories

Apple Developer

Category Paths Strategy
Xcode Derived Data ~/Library/Developer/Xcode/DerivedData Top-level dirs
Xcode Archives ~/Library/Developer/Xcode/Archives Top-level dirs
Device Support ~/Library/Developer/Xcode/*DeviceSupport Top-level dirs
iOS Simulators ~/Library/Developer/CoreSimulator/Devices Top-level dirs
Node Modules ~/Developer, ~/Documents, ~/Desktop, etc. Deep name search
CocoaPods Cache ~/Library/Caches/CocoaPods Top-level dirs
Homebrew Cache ~/Library/Caches/Homebrew Top-level dirs

Android Developer

Category Paths Strategy
Gradle Cache ~/.gradle/caches, ~/.gradle/wrapper/dists Top-level dirs
Android Emulators ~/.android/avd Top-level (files + dirs)
APK / AAB Files ~/ (whole home) Deep file scan (.apk, .aab, .aar)

System

Category Paths Strategy
System Caches ~/Library/Caches Top-level dirs (> 1 MB)
Application Support ~/Library/Application Support Top-level dirs (> 1 MB)
Log Files ~/Library/Logs Top-level dirs

User Files

Category Paths Strategy
Downloads ~/Downloads Top-level (files + dirs)
Images ~/ (whole home) Deep file scan (.jpg, .png, .heic, .webp, …)
Videos ~/ (whole home) Deep file scan (.mp4, .mov, .mkv, .avi, …)
Large Files ~/Documents, ~/Downloads, ~/Desktop Deep file scan (> 100 MB)

Backups

Category Paths Strategy
iOS Backups ~/Library/Application Support/MobileSync/Backup Top-level dirs
Time Machine Snapshots tmutil listlocalsnapshots Shell command

Settings

Accessible via ⌘ , or the gear button in the toolbar.

Tab Contents
General Color scheme (dark/light), default action after scan, deletion safety mode, reset all preferences
Scan Performance mode, global minimum file size, ignored folder names, excluded paths, cache TTL, auto-scan schedule
Categories Drag-and-drop reorder for sidebar and scan progress display
Notifications Enable/disable storage alerts, threshold picker (75 – 95%)
Export Format (CSV / JSON / Plain Text), include hidden categories, default export folder
Custom Scanners Create and manage user-defined scanners by extension + directory

Key Behaviours

  • Old & Unused Files tool — shows files whose modification date is older than a segmented threshold (6, 12, or 24 months). Uses the shared file index when available after a full scan, otherwise merges paths from cached category results.
  • No full re-scan after delete — items are removed from the in-memory list instantly; disk usage is recomputed from current state
  • Background file I/O — deletions run on a detached task; the main actor is suspended (not blocked) so the UI stays at 60 fps
  • Live deletion HUD — a floating progress card shows current file, count, and a progress bar; the list updates row-by-row as each file is removed
  • Ignored patterns are scoped — ignored folder names (e.g. node_modules) skip entries in deepFiles/deepLargeFiles scans; they do not affect the dedicated Node Modules category scanner which specifically targets those folders
  • **.d.ts filtering** — TypeScript declaration files are explicitly excluded from the Videos and other extension-matched scans
  • Thumbnail caching — NSCache backs per-path thumbnail generation; cache is cleared when the app is in the background
  • Auto-scan — uses NSBackgroundActivityScheduler; fires a notification if disk usage exceeds the configured threshold

Permissions

The app runs sandboxed with ENABLE_USER_SELECTED_FILES = readwrite.

On first launch an NSOpenPanel prompts for home directory access. The returned security-scoped URL is retained for the session — this grants read-write access to all subdirectories under ~/.

Full Disk Access (System Settings → Privacy & Security → Full Disk Access) is recommended for complete coverage including ~/Library. Use the lock icon in the toolbar to open the relevant settings pane.

Failed deletions (permission errors, system-protected files) surface a named-item alert rather than silently swallowing the error.


Requirements

  • macOS 14.0 (Sonoma)+
  • Xcode 26.0+ (Swift 6.2 toolchain; the project compiles in Swift 5 language mode)

To build and install a Release copy, see Install.


Adding a New Category

  1. Add a case to StorageCategory in ModelsStorageCategory.swift
  2. Implement scanStyle, title, icon, color, description, and paths(from:) for the new case
  3. The existing scan strategies (topLevelDirs, deepFiles, deepLargeFiles, etc.) cover most cases — only add a new ScanStyle case if none fit
  4. If a new traversal is needed, add a private method to ServicesStorageScanner.swift and a corresponding FTS function in ServicesFastScanEngine.swift

Adding a New Tool

  1. Add a case to ToolType in ModelsToolType.swift (title, icon, color, description)
  2. Create ViewsYourToolView.swift and switch it in ToolRouterView inside ContentView.swift
  3. Add any dedicated scanner logic under Services/ and wire through StorageManager as needed

About

A fast, modern macOS app for scanning, analyzing, and cleaning storage — built for developers and power users.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages