1.4.1: count every byte by default, plus a full correctness and polish pass - #27
Merged
Merged
Conversation
The tree could only produce Developer ID builds: MacDirStat.entitlements turned the sandbox off in 319117c so Full Disk Access would work, and the store refuses anything unsandboxed. The App Store listing sat at 1.0 as a result, four releases behind. Adds a third build configuration that differs from Release in four ways: sandboxed entitlements with user-selected read-write (the treemap and duplicate views trash files, and Save Scan writes a .mdscan), an Info.plist without Sparkle's feed keys, the APPSTORE compilation condition, and -dead_strip_dylibs. Sparkle has to go, not just go quiet: it nests Updater.app and XPC services inside its framework, and nested bundles fail App Store upload validation before review even starts. SwiftPM product dependencies can't be scoped to a configuration, so a build phase strips the embedded framework and re-checks with otool. Xcode signs after that phase runs, so the signature covers the stripped bundle. Build.isAppStore switches off the Full Disk Access probe, the banner, the sheet, the menu item, and the whole Permissions settings section. A sandboxed app can never hold FDA, so every one of those is a dead end that would send users to System Settings for nothing. Versions go to 1.4 / build 10, ahead of both the 1.0 on the store and the v1.3.0 tag on the DMG track.
…ookmark "Auto-scan last folder" stored a bare path and reopened it on launch. A sandboxed build loses access to that folder the moment the process exits, so in an App Store build the setting would silently open onto an empty chart. rememberLastScannedFolder stores a security-scoped bookmark alongside the path while the scope is still held; resolveLastScannedFolder reads the bookmark in App Store builds and the path elsewhere, so the Developer ID behaviour is unchanged. Reopening a .mdscan archive clears the bookmark: it records the archive's scannedPath but grants no access to that folder, so leaving a older bookmark in place would auto-scan a different folder than the one the stored path names.
Archiving needs to pick a configuration, and the auto-generated scheme archived Release, so Product > Archive would have quietly produced the Developer ID build. "DirStat (App Store)" archives the AppStore configuration instead. MacDirStat has to be checked in alongside it: Xcode stops auto-generating schemes as soon as one is shared, and release.yml archives -scheme MacDirStat to build the notarized DMG.
Checked ~/Documents/Projects/MacDirStat-appstore, the second clone the shipped store builds were cut from. Its App Store adaptation lives as uncommitted working-tree edits, and three of them were missing here: - quit on last window closed. The store build did this; the Developer ID build must not, because its Sparkle updater needs to stay resident. One AppDelegate, returning Build.isAppStore. - an in-app Privacy Policy link. App Review wants a reachable policy; added unconditionally since the Developer ID build benefits too (https://ti-03.github.io/MacDirStat/privacy, verified 200). - no CFBundleDocumentTypes. It was an empty array either way. Deliberate deviation: that clone requests files.user-selected.read-only, which does not permit moving files to the Trash, so right-click delete and the duplicates view could not have worked in the shipped store build. This keeps read-write. Flip one line in MacDirStat-AppStore.entitlements to match 1.0 exactly instead.
…ad-only entitlement
The only crypto is CryptoKit SHA256 fingerprinting for duplicate detection. Hashing is not encryption and nothing else encrypts, so the answer is no every time; declaring it skips the export-compliance question on each upload.
…ead of excluding them The 1.3 factory defaults excluded .git, node_modules, DerivedData and .Trash and skipped every dotfile, so a real 7.5 GB project scanned as 40 MB. Nothing is excluded now and hidden entries are counted; known generated trees (node_modules, .git, Pods, .next, .venv, ...) are auto-summarized into one node so totals are right and the treemap stays browsable. Because shipped builds persisted the old list into the preferences file, a one-time migration resets exactly that legacy value; customised lists and the per-user hidden-files toggle are left alone. Shared ScanDefaults replaces four hand-copied default strings, and the splice-refresh guard now uses the same generated-tree list as the scanner. Verified against du on a 7.5 GB project: identical byte total.
…2), stop counting purgeable space as unreadable - A symlinked scan root (/tmp, a Finder alias folder) failed with a raw SkipError description; it is now resolved with realpath(3) and scanned as its target. Subdirectory symlinks are still never followed. - Full Disk Access is detected by actually opening a TCC-protected file, on either the system or the per-user TCC database, instead of access(2) on one path (issue #24: grant present, warning still shown). - The synthetic root child for a volume scan now subtracts the 'important usage' availability Finder reports, so purgeable space (local snapshots, evictable caches) is no longer reported as unreadable.
…dead-end actions - Every failure (scan, save, open, compare, export, trash) set errorMessage but no view ever showed it; ContentView now presents it as an alert, and trash failures report which items could not be moved. - Radial layout skipped any arc under 0.018 rad, so a flat folder with ~350+ similar files drew an empty chart labelled 'no files'. The cutoff is now a 0.75 pt arc length at the outer edge; the empty state keys off the node's children instead of the cell list; the center Back control always shows. - Auto-summarized folders (node_modules, .git, ...) can no longer be drilled into (empty chart, hidden Back); they are selected instead. Tooltips and rows show '12,431 files, summarized' rather than '0 items'. - Clicking empty canvas clears the selection, which also pauses the 30 fps pulse timer that otherwise ran forever after the first click. - The synthetic 'Hidden & Unreadable Space' node no longer offers Reveal, Copy Path or Move to Trash anywhere, and no longer lands in the legend as '(directory)'; extensionless files are labelled '(no extension)'. - Legend recolors with the color scheme; sizes refresh when the unit style changes; ByteFormatter steps up at unit boundaries (no more '1000.0 KB'). - File list: readable selected rows in light mode, expansion state resets when the tree is replaced, sidebar Duplicates link that stranded the user in the detail column removed, duplicate groups keyed by stable group id. - Export CSV disabled with no scan loaded; toolbar shows 'Comparing…'.
…licate finder honest - Duplicate detection grouped by allocated size and treated a 64 KB quick hash as conclusive whenever that size was small. Sparse or APFS-compressed files with identical heads and different tails were reported as duplicates, so 'Delete All Duplicates' could trash a distinct file. The quick hash now records whether it reached end-of-file and only then counts as final. - A scan generation counter guards every main-actor resume after an await: a scan that finishes tagging after the user started another no longer installs its tree, watches the old folder or leaves its hashing running; a slow archive decode no longer flips a newer live scan read-only. - Live-refresh splices (disk walk + full record copy per FSEvents batch) run off the main actor, and are dropped if the tree changed meanwhile, so a splice can no longer resurrect a folder the user just trashed. - Trashing while duplicate detection was still running used to leave 'Scanning for duplicates…' spinning forever; detection now finishes first. - Duplicate rows and list rows that outlive a splice are resolved by path when trashing instead of being silently ignored. - Opening an archive cancels the running scanner, and no longer rewrites the auto-scan breadcrumb to the archive's recorded path. - recomputeLayout clears the busy flag when it has no size to lay out yet (auto-scan on launch showed 'Building treemap…' indefinitely).
…c; wider file list Text was measured inside a capped width but drawn at its natural width, so long names painted over neighbouring arcs. Labels are now measured at natural size, middle-truncated to fit (dropped entirely when fewer than 7 characters or 30% of the name would survive), placed with collision avoidance against every label already drawn, and slid outward within their own band when they would run under the center disc. The file list panel min width goes from 220 to 290 pt so names keep about 120 pt next to the fixed size and percent columns.
… in comparisons, honest gap label - DuplicateDetector.detect returns assignments instead of writing records off the main actor; callers apply them on main. An optional focus set limits a live-refresh re-run to the size buckets the changed subtree touches instead of re-hashing the whole tree every FSEvents batch. - trashNodes performs the moves in a detached task and prunes when they land; the tree is marked stale if it was replaced meanwhile. - ScanComparison suppresses descendants of a directory that is auto-summarized on either side and reports that directory's own grew/shrank row, instead of thousands of false 'removed' rows. - The volume-scan gap row is renamed 'System & Unreadable Space': with hidden files scanned, what remains is other container volumes (Preboot, Recovery, VM), purgeable snapshot space and metadata. Full boot-volume diagnostic: 854.9 GB over 8.5M items against a 907 GB container whose reachable volumes (Data + System) hold 885 GB.
… once FDA is held, fast safety tagging Verified in the installed Developer ID-signed build against the whole boot volume. The non-sandboxed app's preferences still carried 1.3's persisted showHiddenFiles=false, so 260 GB of dot-folders (~/.cache, ~/.ollama, .git, .Trash) were skipped; the one-time migration is now versioned and resets that legacy value too. With it the app shows 855.5 GB of files plus 25.7 GB of other-volume/snapshot space, 881.2 GB total, matching Finder. The 'Full Disk Access required' banner is no longer shown when the grant is held: the remaining denied folders are root-only (issue #24). Safety tagging walks the tree once building paths incrementally instead of allocating a URL per node, which had kept a 5.8M-item scan on 'Scanning…' for two minutes after the walk finished; the panel now says 'Preparing…' during that phase.
`midAngle` is Double while the radii are CGFloat. Combining them inside a single CGPoint expression compiles where CGFloat and Double are the same type, but is 'ambiguous use of cos' on the macos-15 runner. The conversion is now explicit and hoisted, which also stops the placement loop recomputing it per iteration.
The shipped app targets macOS 13 (and has since 1.1), but the README and the download page both advertised 14+, which turns Ventura users away from a release they can run. The 14+/Xcode 15+ requirement applies only to building from source with SwiftPM, so it now says so.
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Ships as 1.4.1 (build 12) on the Mac App Store, and as the next notarized DMG.
The headline fix
The factory defaults excluded
.git,node_modules,DerivedDataand.Trashand skipped every hidden entry, so a real 7.5 GB project reported as 40 MB. Nothing is excluded now and hidden entries are counted; known generated trees collapse into one summary row instead of vanishing. Shipped builds persisted the old settings, so a one-time migration resets exactly those legacy values and leaves customised ones alone.Verified against
du: byte-identical on a 7.5 GB project, and 881.2 GB on a boot volume Finder reports at 880 GB used.Also fixed
access(2)on one path, so a granted app still showed the banner (Permissions are not recognized and are not being used (Full disc access) #24). Purgeable space was reported as unreadable.Full list in
CHANGELOG.mdunder 1.4.1.Release engineering
MARKETING_VERSION1.4 → 1.4.1,CURRENT_PROJECT_VERSION10 → 12.docs/adr/0002-two-distribution-tracks.md; both schemes checked in.Build 12 is uploaded to App Store Connect and attached to version 1.4.1.