Skip to content

Commit b50a8a8

Browse files
committed
fix: orphan context menu actions + app size calculation (#96, #92)
#96: OrphanListView's only context menu action was Reveal in Finder, which called NSWorkspace.selectFile(_:inFileViewerRootedAtPath: "") - this silently no-ops when the path is unreachable from Finder's scope (sandbox boundaries, missing parent, etc), so users right-clicked an orphan, nothing happened, and there was no other way to act on a single row. - Switch to NSWorkspace.activateFileViewerSelecting([url]) which handles sandbox-bookmarked paths. - Fall back to opening the enclosing directory when the file itself has disappeared since the scan. - Add Copy Path (diagnostic / share with maintainer). - Add Move to Trash so a single row can be removed without the select-then-toolbar dance. #92: AppInfoFetcher.appSize(at:) had a fast-path that read totalFileAllocatedSizeKey on the bundle directory URL. On APFS that returns only the directory inode size (~4 KB), not the recursive sum, so size > 0 was true and the function returned ~4 KB and never enumerated - every installed app's size was reported as a few kilobytes. - Drop the broken fast-path; always enumerate. - Skip symlinks (avoids double-counting and following links out of the bundle). - Drop the 10 000-file cap (Xcode, MS Office, Adobe apps all exceed it). - Sum totalFileAllocatedSize per regular file, falling back to fileAllocatedSize. Adds Copy Path and Move to Trash keys to all 7 bundled locales (en/es/ja/ar/pt-BR/zh-Hans/zh-Hant) so LocalizationFilesTests stays green. Verified locally: xcodebuild build succeeds, LocalizationFilesTests passes.
1 parent 5e5d257 commit b50a8a8

9 files changed

Lines changed: 62 additions & 16 deletions

File tree

PureMac/Logic/Scanning/AppInfoFetcher.swift

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -103,30 +103,29 @@ final class AppInfoFetcher {
103103
}
104104

105105
private func appSize(at url: URL) -> Int64 {
106-
// Try totalFileAllocatedSize on the bundle URL first (fast, accurate)
107-
if let values = try? url.resourceValues(forKeys: [.totalFileAllocatedSizeKey]),
108-
let size = values.totalFileAllocatedSize, size > 0 {
109-
return Int64(size)
110-
}
111-
112-
// Enumerate files and sum their sizes
106+
// totalFileAllocatedSizeKey on a directory URL returns only the
107+
// directory inode (~4 KB on APFS), not the recursive sum - the
108+
// previous fast-path returned that and exited, causing app sizes
109+
// to display as ~4 KB regardless of bundle contents. Always
110+
// enumerate the bundle contents and sum.
113111
guard let enumerator = fileManager.enumerator(
114112
at: url,
115-
includingPropertiesForKeys: [.totalFileAllocatedSizeKey, .isRegularFileKey],
113+
includingPropertiesForKeys: [.totalFileAllocatedSizeKey, .fileAllocatedSizeKey, .isRegularFileKey, .isSymbolicLinkKey],
116114
options: [.skipsHiddenFiles]
117115
) else { return 0 }
118116

119117
var total: Int64 = 0
120-
var count = 0
121118
for case let fileURL as URL in enumerator {
122-
count += 1
123-
if count > 10000 { break }
124-
guard let values = try? fileURL.resourceValues(forKeys: [.totalFileAllocatedSizeKey, .fileSizeKey, .isRegularFileKey]),
125-
values.isRegularFile == true else { continue }
119+
guard let values = try? fileURL.resourceValues(forKeys: [.totalFileAllocatedSizeKey, .fileAllocatedSizeKey, .isRegularFileKey, .isSymbolicLinkKey]) else { continue }
120+
// Skip symlinks so we don't double-count or follow links out of
121+
// the bundle. Skip directories so we only count regular file
122+
// payload.
123+
if values.isSymbolicLink == true { continue }
124+
guard values.isRegularFile == true else { continue }
126125
if let allocated = values.totalFileAllocatedSize {
127126
total += Int64(allocated)
128-
} else if let size = values.fileSize {
129-
total += Int64(size)
127+
} else if let allocated = values.fileAllocatedSize {
128+
total += Int64(allocated)
130129
}
131130
}
132131
return total

PureMac/Views/Orphans/OrphanListView.swift

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,15 @@ struct OrphanListView: View {
4747
.toggleStyle(.checkbox)
4848
.contextMenu {
4949
Button("Reveal in Finder") {
50-
NSWorkspace.shared.selectFile(fileURL.path, inFileViewerRootedAtPath: "")
50+
revealInFinder(fileURL)
51+
}
52+
Button("Copy Path") {
53+
NSPasteboard.general.clearContents()
54+
NSPasteboard.general.setString(fileURL.path, forType: .string)
55+
}
56+
Divider()
57+
Button("Move to Trash", role: .destructive) {
58+
Task { await removeSingleOrphan(fileURL) }
5159
}
5260
}
5361
}
@@ -204,6 +212,31 @@ struct OrphanListView: View {
204212
}
205213
}
206214

215+
private func revealInFinder(_ url: URL) {
216+
// activateFileViewerSelecting handles sandbox-bookmarked paths and
217+
// missing files better than selectFile(_:inFileViewerRootedAtPath:),
218+
// which silently no-ops when the path is unreachable from Finder's
219+
// current scope. If the target itself was removed since the scan,
220+
// fall back to opening the enclosing directory so the user lands
221+
// somewhere useful instead of nothing happening.
222+
let fm = FileManager.default
223+
if fm.fileExists(atPath: url.path) {
224+
NSWorkspace.shared.activateFileViewerSelecting([url])
225+
return
226+
}
227+
let parent = url.deletingLastPathComponent()
228+
if fm.fileExists(atPath: parent.path) {
229+
NSWorkspace.shared.open(parent)
230+
}
231+
}
232+
233+
private func removeSingleOrphan(_ url: URL) async {
234+
let previous = selectedOrphans
235+
selectedOrphans = [url]
236+
await removeSelectedOrphans()
237+
selectedOrphans = previous.subtracting([url])
238+
}
239+
207240
private func removeWithAdminPrivileges(_ urls: [URL]) -> Bool {
208241
guard !urls.isEmpty else { return true }
209242
guard urls.allSatisfy({ OrphanSafetyPolicy.isSafeCandidate($0) }) else { return false }

PureMac/ar.lproj/Localizable.strings

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@
8383
"Rescan" = "إعادة الفحص";
8484
"%lld of %lld selected" = "%lld من %lld محدد";
8585
"Reveal in Finder" = "إظهار في Finder";
86+
"Copy Path" = "نسخ المسار";
87+
"Move to Trash" = "نقل إلى المهملات";
8688

8789
/* Apps (AppListView) */
8890
"Search apps" = "بحث في التطبيقات";

PureMac/en.lproj/Localizable.strings

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@
8383
"Rescan" = "Rescan";
8484
"%lld of %lld selected" = "%lld of %lld selected";
8585
"Reveal in Finder" = "Reveal in Finder";
86+
"Copy Path" = "Copy Path";
87+
"Move to Trash" = "Move to Trash";
8688

8789
/* Apps (AppListView) */
8890
"Search apps" = "Search apps";

PureMac/es.lproj/Localizable.strings

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@
8383
"Rescan" = "Volver a analizar";
8484
"%lld of %lld selected" = "%lld de %lld seleccionados";
8585
"Reveal in Finder" = "Mostrar en el Finder";
86+
"Copy Path" = "Copiar ruta";
87+
"Move to Trash" = "Mover a la Papelera";
8688

8789
/* Apps (AppListView) */
8890
"Search apps" = "Buscar apps";

PureMac/ja.lproj/Localizable.strings

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@
8383
"Rescan" = "再スキャン";
8484
"%lld of %lld selected" = "%lld / %lld 個を選択中";
8585
"Reveal in Finder" = "Finderで表示";
86+
"Copy Path" = "パスをコピー";
87+
"Move to Trash" = "ゴミ箱に移動";
8688

8789
/* Apps (AppListView) */
8890
"Search apps" = "アプリを検索";

PureMac/pt-BR.lproj/Localizable.strings

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@
8383
"Rescan" = "Verificar novamente";
8484
"%lld of %lld selected" = "%lld de %lld selecionados";
8585
"Reveal in Finder" = "Mostrar no Finder";
86+
"Copy Path" = "Copiar caminho";
87+
"Move to Trash" = "Mover para o Lixo";
8688

8789
/* Apps (AppListView) */
8890
"Search apps" = "Buscar apps";

PureMac/zh-Hans.lproj/Localizable.strings

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@
8383
"Rescan" = "重新扫描";
8484
"%lld of %lld selected" = "已选 %lld / %lld";
8585
"Reveal in Finder" = "在访达中显示";
86+
"Copy Path" = "复制路径";
87+
"Move to Trash" = "移到废纸篓";
8688

8789
/* Apps (AppListView) */
8890
"Search apps" = "搜索应用";

PureMac/zh-Hant.lproj/Localizable.strings

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@
8383
"Rescan" = "重新掃描";
8484
"%lld of %lld selected" = "已選 %lld / %lld";
8585
"Reveal in Finder" = "在 Finder 中顯示";
86+
"Copy Path" = "複製路徑";
87+
"Move to Trash" = "移到垃圾桶";
8688

8789
/* Apps (AppListView) */
8890
"Search apps" = "搜尋應用程式";

0 commit comments

Comments
 (0)