feat(prune): R3 guarded deletion — confirm-to-delete + panel button

Extend BANK_PRUNE_FOLDER from report-only to dry-run→confirm-with-manifest→
delete exactly the pure-core orphan set (fresh recompute, stale entries skipped).
Windows routes to Recycle Bin (SHFileOperation+FOF_ALLOWUNDO), else unlink.
New pure prune_button module + footer button dispatching the action id. No
ext-state write, no undo point (deletion is not REAPER-undoable).
This commit is contained in:
2026-07-26 18:47:02 -04:00
parent 105a4421a8
commit 7b53ba16f6
12 changed files with 695 additions and 28 deletions
+49 -11
View File
@@ -797,29 +797,65 @@ void doBankRemoveSelected() {
if (removed > 0) persistBankOp("ReaSampler: remove sample(s)");
}
// Prune bank folder — Phase R (Reclaim), R2: REPORT-ONLY dry run. Asks the session for
// the orphan set of the resolved CURRENT bank folder ((owned ∩ present) referenced,
// unioned across every bank) and prints the truthful reclaim report — count, reclaimable
// bytes, and the file list. DELETES NOTHING, writes no ext-state, opens no undo point
// (pruneDryRun is read-only across the persist seam). R3 extends this SAME action id to
// confirm-and-delete behind the dry-run guardrail; the report path here is what R3 wraps.
// Prune bank folder — Phase R (Reclaim), R3: the guarded DESTRUCTIVE step, and the SOLE
// file-deletion entry in ReaSampler. Dry-run FIRST (compute the orphan set, read-only),
// then — only when orphans exist — a blocking CONFIRM showing the SPECIFIC manifest
// (count + reclaimable bytes + the file list, truncated consistent with the 64-cap), then
// on explicit Yes delete EXACTLY that set (session->pruneReclaim, which recomputes the
// pure core fresh and deletes confirmed ∩ freshOrphans — trash-preferred, unlink fallback).
// Zero orphans => informational only, NO confirm ever shown. Cancel deletes nothing.
//
// The full (untruncated) orphan set is captured here for the delete; the dry-run's
// truncated list is only the confirm's readout. No ext-state is written and no undo point
// is opened (file deletion is not REAPER-undoable and pruneReclaim mutates no project
// state) — a Ctrl-Z after a prune correctly cannot claim to restore deleted files.
void doBankPruneFolder() {
const PruneReport report = g_session->pruneDryRun();
if (report.count == 0) {
ShowConsoleMsg("ReaSampler prune (dry run): no orphaned files to reclaim.\n");
ShowConsoleMsg("ReaSampler prune: no orphaned files to reclaim.\n");
return;
}
std::string msg = "ReaSampler prune (dry run): " + std::to_string(report.count) +
" orphaned file(s), " + std::to_string(report.totalBytes) +
" bytes reclaimable. (Dry run -- nothing deleted.)\n";
// The EXACT set the delete will target — full, untruncated, so what the confirm
// summarises (count + bytes) matches what pruneReclaim reclaims. Captured before the
// confirm so the confirm and the delete reason about the same enumeration.
const std::vector<std::string> orphanSet = g_session->pruneOrphanSet();
// Confirm-with-manifest: count + bytes exact; the file list is the dry-run's 64-capped
// list (the same clip the R2 readout used), with a "N more not shown" tail when clipped.
std::string msg =
"ReaSampler prune will PERMANENTLY reclaim " + std::to_string(report.count) +
" orphaned file(s), freeing " + std::to_string(report.totalBytes) + " bytes.\n\n"
"These files are no longer referenced by any bank and were created by ReaSampler.\n"
"They will be moved to the Recycle Bin on Windows (recoverable), or deleted on "
"other platforms.\n\n";
for (const std::string& rel : report.orphans) msg += " " + rel + "\n";
if (report.truncated) {
msg += " ... (" + std::to_string(report.count - report.orphans.size()) +
" more not shown)\n";
}
ShowConsoleMsg(msg.c_str());
msg += "\nReclaim these files now?";
const int r = ShowMessageBox(msg.c_str(), "ReaSampler: prune bank folder", 4);
if (r != 6) { // 6 == YES; anything else cancels -> delete NOTHING (SDK ~6544)
ShowConsoleMsg("ReaSampler prune: cancelled -- nothing deleted.\n");
return;
}
// Confirmed -> delete exactly the confirmed set (recomputed fresh, stale entries skipped).
const PruneDeletionResult del = g_session->pruneReclaim(orphanSet);
std::string done = "ReaSampler prune: reclaimed " +
std::to_string(del.reclaimedCount) + " file(s), " +
std::to_string(del.reclaimedBytes) + " bytes" +
(del.usedTrash ? " (to Recycle Bin)" : " (deleted)") + ".";
if (del.skippedCount > 0) {
done += " " + std::to_string(del.skippedCount) +
" file(s) skipped (locked, or changed since the report).";
}
done += "\n";
ShowConsoleMsg(done.c_str());
}
} // namespace
@@ -907,6 +943,8 @@ bool bankHandleCommand(int command) {
return false; // not ours — caller's hookcommand keeps looking
}
int bankPruneCommandId() { return g_cmdBankPruneFolder; }
void bankUnregisterActions(reaper_plugin_info_t* rec) {
// Mirror-unregister with '-'-prefixed strings, reverse of registration order. Each
// '-command_id' re-presents the same interned channel-qualified id (channelIdFor).