Skip to content

1.4.1: count every byte by default, plus a full correctness and polish pass - #27

Merged
Ti-03 merged 19 commits into
mainfrom
release/app-store-1.4
Sep 16, 2026
Merged

Ti-03 merged 19 commits into
mainfrom
release/app-store-1.4

Conversation

@Ti-03

@Ti-03 Ti-03 commented Sep 16, 2026

Copy link
Copy Markdown
Owner

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, DerivedData and .Trash and 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

  • Duplicate finder could group two different files that merely shared an allocated size and first 64 KB, so "Delete All Duplicates" could trash a distinct file.
  • Every failure (scan, save, open, compare, export, trash) was recorded but never displayed. They now surface as an alert.
  • A flat folder with a few hundred similar files drew an empty chart labelled "no files".
  • Auto-summarized folders could be drilled into, leaving an empty chart with the Back control hidden.
  • A scan finishing during a newer scan or archive load could install its results over the newer one.
  • Live refresh did its disk walk on the main thread and could resurrect a just-trashed folder.
  • Full Disk Access was detected with 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.
  • Chart labels overlapped and spilled past their arcs; the file list truncated names to three letters.
  • Duplicate detection after a live change now hashes only affected size buckets; Move to Trash runs off the main thread.

Full list in CHANGELOG.md under 1.4.1.

Release engineering

  • MARKETING_VERSION 1.4 → 1.4.1, CURRENT_PROJECT_VERSION 10 → 12.
  • Two distribution tracks documented in docs/adr/0002-two-distribution-tracks.md; both schemes checked in.
  • 156 tests, 0 failures. Release and AppStore configurations both build.

Build 12 is uploaded to App Store Connect and attached to version 1.4.1.

Ti-03 added 19 commits August 25, 2026 12:00
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.
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.
@Ti-03
Ti-03 merged commit 921d582 into main Sep 16, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant