Skip to content

Commit 07f03d0

Browse files
committed
Release v0.8.1: UX polish + WizTree-speed disk analyzer
Startup overlay: - Replace the indeterminate spinner with a live percentage readout (42pt) and a determinate progress bar bound to a shared step counter. - Each of the 11 parallel scanners ticks the overall progress as it finishes, so the user sees real movement instead of a spinning ring. Disk Analyzer: - New FastDiskAnalyzer uses WizTree's technique: open the volume with CreateFile(\.\C:, FILE_READ_DATA | FILE_READ_ATTRIBUTES), enumerate every file via FSCTL_ENUM_USN_DATA in one sequential pass, then pull sizes in a single FSCTL_GET_NTFS_FILE_RECORD pass that parses the \$STANDARD_INFORMATION + \$DATA attributes inline. - Reconstructs absolute paths by walking the ParentFRN chain up to the NTFS root FRN (0x5). - Non-NTFS and permission-failure fallback: parallel FindFirstFileExW with FIND_FIRST_EX_LARGE_FETCH and FindExInfoBasic. Still materially faster than Directory.EnumerateFiles because the large-fetch flag streams more entries per syscall and we skip the 8.3 short-name lookup. - Scan time appears in the status bar. Registry Hunter: - Parallel fan-out across HKLM, HKLM\WOW6432Node, HKCU, and HKCR on independent worker tasks with a ConcurrentBag sink. - Scope filter (Keys / Value names / Value data) — lifted from NirSoft RegScanner — so a CLSID hunt doesn't get drowned in install-path data matches and vice versa. - Optional compiled regex pattern (RegexOptions.Compiled | IgnoreCase with 1s match timeout). Falls back to substring if the pattern won't compile. - Live hit counter streams to the UI every 32 matches. - Struct-based matcher avoids per-call delegate-invocation overhead in the hot recursion path. Uninstall flow: - Successful single-program uninstalls now remove the row from the Programs list immediately instead of leaving stale entries. A full registry rescan remains behind the Refresh button for cases where an uninstaller lies. Version: - 0.8.0 -> 0.8.1 synced across csproj x2, manifest, README, XAML, BUILD.bat, Build.ps1, CHANGELOG.
1 parent 4e21bc3 commit 07f03d0

12 files changed

Lines changed: 1124 additions & 137 deletions

File tree

BUILD.bat

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
@echo off
2-
title DeepPurge Builder v0.8.0
2+
title DeepPurge Builder v0.8.1
33
echo.
44
echo ============================================
55
echo DeepPurge Builder

Build.ps1

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<#
22
.SYNOPSIS
3-
DeepPurge Build Script v0.8.0
3+
DeepPurge Build Script v0.8.1
44
Compiles the project into a single portable .exe
55
66
.DESCRIPTION
@@ -29,7 +29,7 @@ $AppProject = Join-Path $ProjectRoot "src\DeepPurge.App\DeepPurge.App.csproj"
2929

3030
Write-Host ""
3131
Write-Host " ============================================" -ForegroundColor Cyan
32-
Write-Host " DeepPurge Build Script v0.8.0" -ForegroundColor Cyan
32+
Write-Host " DeepPurge Build Script v0.8.1" -ForegroundColor Cyan
3333
Write-Host " ============================================" -ForegroundColor Cyan
3434
Write-Host ""
3535

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22

33
All notable changes to DeepPurge will be documented in this file.
44

5+
## [v0.8.1] — UX polish + WizTree-speed disk analyzer
6+
7+
### Added
8+
- **Startup shows a real percentage** — the spinning circle on the loading screen is replaced by a big live "N%" readout plus a determinate progress bar. Each of the 11 scan phases ticks the bar as it finishes so the user can see what's happening instead of just a looping animation.
9+
- **Disk Analyzer now uses WizTree's MFT technique** — new `FastDiskAnalyzer` reads the raw NTFS `$MFT` via `FSCTL_ENUM_USN_DATA` in one sequential sweep, then pulls sizes in a single `FSCTL_GET_NTFS_FILE_RECORD` pass. One warm volume handle replaces millions of random-seek `FindFirstFile` calls. Non-NTFS volumes fall back to a parallel `FindFirstFileExW` walk with the `FIND_FIRST_EX_LARGE_FETCH` hint and `FindExInfoBasic` (skips the 8.3 short-name lookup) — still materially faster than `Directory.EnumerateFiles`. Scan time appears in the status bar.
10+
- **Registry Hunter rewritten along NirSoft RegScanner / Eric Zimmerman lines** — now scans HKLM, HKLM\\WOW6432Node, HKCU, and HKCR in parallel; adds a scope filter (Keys / Value names / Value data); adds optional compiled regex for pattern matching; streams a live hit counter to the UI every 32 matches. Same hit / depth / time caps as before so unbounded searches can't melt the process.
11+
12+
### Fixed
13+
- **Uninstalled programs now disappear from the list immediately** after a successful uninstall. No need to hit Refresh to see the row go away; the underlying engine still honours the registry on rescan so broken-uninstaller cases don't pretend to succeed.
14+
515
## [v0.8.0] — Competitive feature pass
616

717
Research-driven feature pass inspired by BCUninstaller, Revo Uninstaller, BleachBit, PrivaZer, and Sysinternals Autoruns.

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88
</p>
99
<!-- codex-branding:end -->
1010

11-
# DeepPurge v0.8.0
11+
# DeepPurge v0.8.1
1212

13-
![Version](https://img.shields.io/badge/version-v0.8.0-blue) ![License](https://img.shields.io/badge/license-MIT-green) ![Platform](https://img.shields.io/badge/platform-Windows%2010%2F11-lightgrey)
13+
![Version](https://img.shields.io/badge/version-v0.8.1-blue) ![License](https://img.shields.io/badge/license-MIT-green) ![Platform](https://img.shields.io/badge/platform-Windows%2010%2F11-lightgrey)
1414

1515
A thorough, open-source Windows uninstaller that goes deep. Removes programs completely, hunts down every leftover, and cleans system cruft that other tools miss.
1616

@@ -31,7 +31,7 @@ A thorough, open-source Windows uninstaller that goes deep. Removes programs com
3131
- **Junk Cleaner** - Browser caches, temp files, crash dumps, prefetch, installer cache, Windows Update leftovers
3232
- **Evidence Remover** - Recent documents, jump lists, thumbnail cache, clipboard, DNS cache, Explorer history, Windows logs, crash reports, error reports, font cache, delivery optimization cache
3333
- **Empty Folders** - Scan common locations for empty directory trees and remove them
34-
- **Disk Analyzer** - Folder size breakdown and large file finder (50MB+) with delete capability
34+
- **Disk Analyzer** - Folder size breakdown and large file finder (50MB+) with delete capability. Uses WizTree's raw-MFT technique (`FSCTL_ENUM_USN_DATA` + `FSCTL_GET_NTFS_FILE_RECORD`) on NTFS volumes; parallel `FindFirstFileExW(FIND_FIRST_EX_LARGE_FETCH)` fallback on ReFS/FAT32. Typical full-drive scan in seconds.
3535
- **Dry-run / Preview mode** - Every destructive pipeline can be previewed: enumerate and size items without touching them *(inspired by BleachBit)*
3636
- **Secure Delete** - Privacy-grade wipe (single-pass cryptographic random + opaque rename + delete — multi-pass DoD wipes are obsolete on SSDs and deliberately omitted) *(inspired by BleachBit/PrivaZer)*
3737
- **Live progress bars** - Every long-running delete reports item / total / bytes-freed / current path in the status bar
@@ -43,7 +43,7 @@ A thorough, open-source Windows uninstaller that goes deep. Removes programs com
4343
- **Context Menu Cleaner** - Find and remove orphaned shell context menu entries with broken executables or CLSIDs
4444
- **Services Manager** - View all Windows services, identify orphaned services pointing to deleted executables, disable or delete
4545
- **Scheduled Tasks** - Full task inventory with orphan detection, disable and delete capabilities
46-
- **Registry Hunter** - Arbitrary-substring search across HKLM / HKCU / HKCR with depth, hit, and time caps *(inspired by Revo's trace scanner)*
46+
- **Registry Hunter** - Parallel substring or regex search across HKLM, HKLM\\WOW6432Node, HKCU, and HKCR with scope filters (keys / names / data), live hit counter, and depth / hit / time caps *(inspired by NirSoft RegScanner and Eric Zimmerman's Registry Explorer)*
4747

4848
### Safety
4949
- **System Restore Points** - View, create, and manage restore points

src/DeepPurge.App/App.xaml.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ namespace DeepPurge.App;
77

88
public partial class App : Application
99
{
10-
private const string Version = "0.8.0";
10+
private const string Version = "0.8.1";
1111
private static readonly string CrashLogDir = Path.Combine(
1212
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
1313
"DeepPurge", "Logs");

src/DeepPurge.App/DeepPurge.App.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
<Nullable>enable</Nullable>
99
<RootNamespace>DeepPurge.App</RootNamespace>
1010
<AssemblyName>DeepPurge</AssemblyName>
11-
<Version>0.8.0</Version>
11+
<Version>0.8.1</Version>
1212
<Authors>SysAdminDoc</Authors>
1313
<Description>DeepPurge - Open source program uninstaller for Windows</Description>
1414
<ApplicationIcon>..\..\icon.ico</ApplicationIcon>

src/DeepPurge.App/ViewModels/MainViewModel.cs

Lines changed: 96 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,22 @@ public async Task RunInitialScanAsync()
136136

137137
IsInitialScanRunning = true;
138138
ScanOverlayText = "DeepPurge is analyzing your system...";
139+
OverlayScanProgress = 0;
140+
141+
// 11 phases: programs + 10 parallel scanners. Each tick moves the
142+
// overlay progress a fixed fraction so the user sees real movement
143+
// instead of a perpetually-spinning circle.
144+
const int totalSteps = 11;
145+
int completedSteps = 0;
146+
void Tick(string? stepLabel = null)
147+
{
148+
var done = Interlocked.Increment(ref completedSteps);
149+
_dispatcher.BeginInvoke(() =>
150+
{
151+
OverlayScanProgress = 100.0 * done / totalSteps;
152+
if (!string.IsNullOrEmpty(stepLabel)) ScanOverlayText = stepLabel;
153+
});
154+
}
139155

140156
try
141157
{
@@ -153,17 +169,19 @@ public async Task RunInitialScanAsync()
153169
ProgramsScanProgress = 100;
154170
_loadedPanels.Add("Programs");
155171
});
172+
Tick($"Loaded {programs.Count} programs — scanning the rest...");
156173

157174
StartIconBackfill(programs);
158175
StartPackageEnrichment(programs);
159176

160-
ScanOverlayText = "Scanning junk, tasks, autorun, extensions...";
161-
177+
// Each parallel task calls Tick() on completion so the progress
178+
// bar advances as individual scanners finish, not just at the end.
162179
var junkTask = Task.Run(() =>
163180
{
164181
_dispatcher.BeginInvoke(() => JunkScanProgress = 10);
165182
var result = JunkFilesCleaner.ScanForJunk();
166183
_dispatcher.BeginInvoke(() => JunkScanProgress = 100);
184+
Tick("Junk categories discovered...");
167185
return result;
168186
}, ct);
169187

@@ -172,17 +190,18 @@ public async Task RunInitialScanAsync()
172190
_dispatcher.BeginInvoke(() => TasksScanProgress = 10);
173191
var result = ScheduledTaskScanner.GetAllTasks();
174192
_dispatcher.BeginInvoke(() => TasksScanProgress = 100);
193+
Tick("Scheduled tasks enumerated...");
175194
return result;
176195
}, ct);
177196

178-
var autorunTask = Task.Run(() => AutorunScanner.GetAllAutoruns(), ct);
179-
var extTask = Task.Run(() => BrowserExtensionScanner.GetAllExtensions(), ct);
180-
var appsTask = WindowsAppManager.GetInstalledAppsAsync();
181-
var evidenceTask = Task.Run(() => EvidenceRemover.ScanAllTraces(), ct);
182-
var emptyTask = Task.Run(() => EmptyFolderScanner.ScanCommonLocations(), ct);
183-
var ctxTask = Task.Run(() => ContextMenuCleaner.ScanContextMenuEntries(), ct);
184-
var svcTask = Task.Run(() => ServiceScanner.GetAllServices(), ct);
185-
var restoreTask = Task.Run(() => SystemRestoreManager.GetRestorePoints(), ct);
197+
var autorunTask = Task.Run(() => { var r = AutorunScanner.GetAllAutoruns(); Tick("Autoruns + signatures checked..."); return r; }, ct);
198+
var extTask = Task.Run(() => { var r = BrowserExtensionScanner.GetAllExtensions(); Tick("Browser extensions scanned..."); return r; }, ct);
199+
var appsTask = WindowsAppManager.GetInstalledAppsAsync().ContinueWith(t => { Tick("Windows Apps enumerated..."); return t.Result; }, ct);
200+
var evidenceTask= Task.Run(() => { var r = EvidenceRemover.ScanAllTraces(); Tick("Privacy traces scanned..."); return r; }, ct);
201+
var emptyTask = Task.Run(() => { var r = EmptyFolderScanner.ScanCommonLocations(); Tick("Empty folders scanned..."); return r; }, ct);
202+
var ctxTask = Task.Run(() => { var r = ContextMenuCleaner.ScanContextMenuEntries(); Tick("Shell context menu scanned..."); return r; }, ct);
203+
var svcTask = Task.Run(() => { var r = ServiceScanner.GetAllServices(); Tick("Services + signatures checked..."); return r; }, ct);
204+
var restoreTask = Task.Run(() => { var r = SystemRestoreManager.GetRestorePoints(); Tick("Restore points loaded..."); return r; }, ct);
186205

187206
await Task.WhenAll(junkTask, tasksTask, autorunTask, extTask, appsTask,
188207
evidenceTask, emptyTask, ctxTask, svcTask, restoreTask);
@@ -199,6 +218,7 @@ await Task.WhenAll(junkTask, tasksTask, autorunTask, extTask, appsTask,
199218
ApplyContextMenuResults(ctxTask.Result);
200219
ApplyServiceResults(svcTask.Result);
201220
ApplyRestoreResults(restoreTask.Result);
221+
OverlayScanProgress = 100;
202222
});
203223

204224
ScanOverlayText = "Scan complete!";
@@ -408,20 +428,28 @@ public async Task DeleteEmptyFoldersAsync(IEnumerable<EmptyFolderInfo> selected)
408428

409429
public async Task ScanDiskAsync()
410430
{
411-
IsBusy = true; StatusText = "Analyzing disk space...";
431+
IsBusy = true;
432+
StatusText = "Analyzing disk space (WizTree-style MFT scan)...";
433+
434+
var sw = Stopwatch.StartNew();
412435
try
413436
{
414-
var folders = await Task.Run(() => DiskSpaceAnalyzer.AnalyzeFolder(@"C:\", 1));
415-
var large = await Task.Run(() => DiskSpaceAnalyzer.FindLargeFiles(@"C:\", 50 * 1024 * 1024, 200));
437+
// FastDiskAnalyzer tries a raw NTFS MFT scan first (single sequential
438+
// read per volume) and falls back to FindFirstFileExW with the
439+
// large-fetch flag — both substantially faster than the old
440+
// Directory.EnumerateFiles path.
441+
var folders = await Task.Run(() => FastDiskAnalyzer.AnalyzeDrive(@"C:\"));
442+
var large = await Task.Run(() => FastDiskAnalyzer.FindLargeFiles(@"C:\", 50 * 1024 * 1024, 200));
416443

417444
DiskFolders.Clear();
418445
foreach (var f in folders) DiskFolders.Add(f);
419446

420447
LargeFiles.Clear();
421448
foreach (var f in large) LargeFiles.Add(f);
422449

450+
sw.Stop();
423451
StatusText = $"Found {folders.Count} top-level folders, {large.Count} large files " +
424-
$"({FormatSize(large.Sum(f => f.SizeBytes))})";
452+
$"({FormatSize(large.Sum(f => f.SizeBytes))}) in {sw.Elapsed.TotalSeconds:F1}s";
425453
}
426454
finally { IsBusy = false; }
427455
}
@@ -740,25 +768,60 @@ private void StartPackageEnrichment(IReadOnlyList<InstalledProgram> programs)
740768

741769
public ObservableCollection<RegistryHit> RegistryHits { get; } = new();
742770

771+
[ObservableProperty] private bool _hunterUseRegex;
772+
[ObservableProperty] private bool _hunterSearchKeys = true;
773+
[ObservableProperty] private bool _hunterSearchNames = true;
774+
[ObservableProperty] private bool _hunterSearchData = true;
775+
[ObservableProperty] private int _hunterLiveCount;
776+
743777
public async Task<int> HuntRegistryAsync(string needle)
744778
{
745779
RegistryHits.Clear();
780+
HunterLiveCount = 0;
781+
746782
if (string.IsNullOrWhiteSpace(needle) || needle.Length < 3)
747783
{
748784
StatusText = "Enter at least 3 characters to search the registry";
749785
return 0;
750786
}
751787

788+
var scope = RegistrySearchScope.All;
789+
scope = (HunterSearchKeys ? scope : scope & ~RegistrySearchScope.Keys);
790+
scope = (HunterSearchNames ? scope : scope & ~RegistrySearchScope.Names);
791+
scope = (HunterSearchData ? scope : scope & ~RegistrySearchScope.Data);
792+
793+
if (scope == 0)
794+
{
795+
StatusText = "Pick at least one search target (keys / names / data)";
796+
return 0;
797+
}
798+
799+
var options = new RegistryHuntOptions(
800+
Scope: scope,
801+
UseRegex: HunterUseRegex,
802+
MaxHits: 500);
803+
752804
_cts = new CancellationTokenSource();
753805
IsBusy = true;
754806
StatusText = $"Searching registry for '{needle}'...";
807+
var sw = Stopwatch.StartNew();
808+
755809
try
756810
{
757-
var hits = await Task.Run(() => RegistryHunter.Search(needle, ct: _cts.Token), _cts.Token);
811+
var progress = new Progress<int>(count =>
812+
_dispatcher.BeginInvoke(() => HunterLiveCount = count));
813+
814+
var hits = await Task.Run(
815+
() => RegistryHunter.Search(needle, options, progress, _cts.Token),
816+
_cts.Token);
817+
758818
foreach (var h in hits) RegistryHits.Add(h);
759-
StatusText = hits.Count >= 500
760-
? $"Registry hunter: found 500+ matches for '{needle}' (capped)"
761-
: $"Registry hunter: {hits.Count} matches for '{needle}'";
819+
HunterLiveCount = hits.Count;
820+
821+
sw.Stop();
822+
StatusText = hits.Count >= options.MaxHits
823+
? $"Registry hunter: {options.MaxHits}+ matches for '{needle}' (capped) in {sw.Elapsed.TotalSeconds:F1}s"
824+
: $"Registry hunter: {hits.Count} matches for '{needle}' in {sw.Elapsed.TotalSeconds:F1}s";
762825
return hits.Count;
763826
}
764827
finally
@@ -804,6 +867,21 @@ private void UpdateOperationProgress(DeleteProgress p, string verb)
804867
});
805868
}
806869

870+
/// <summary>
871+
/// Remove a program from the visible lists without re-scanning the entire
872+
/// registry. Called by the view after a successful uninstall so the user
873+
/// sees the program disappear immediately instead of staring at a stale
874+
/// row. A full <see cref="RefreshAsync"/> is still available behind the
875+
/// Refresh button if the uninstaller lied about success.
876+
/// </summary>
877+
public void RemoveProgramFromList(InstalledProgram program)
878+
{
879+
Programs.Remove(program);
880+
FilteredPrograms.Remove(program);
881+
ProgramCountText = $"{Programs.Count} programs";
882+
ProgramsBadge = Programs.Count.ToString();
883+
}
884+
807885
public void OpenBackupFolder()
808886
{
809887
var dir = new Core.Safety.BackupManager().BackupDirectory;

0 commit comments

Comments
 (0)