bin/analyze.ps1
|
# WinMole - Interactive Disk Space Analyzer # Arrow-key navigation, size visualization, open/delete actions param( [string]$Path = '', [switch]$Json ) $ErrorActionPreference = 'SilentlyContinue' . "$PSScriptRoot\..\lib\core.ps1" . "$PSScriptRoot\..\lib\ui.ps1" # ── Resolve start path ──────────────────────────────────────────────────────── if (-not $Path) { $Path = $env:USERPROFILE } $Path = $Path.TrimEnd('\','/') if (-not (Test-Path $Path)) { Write-Err "Path not found: $Path" exit 1 } # ── Scan top-level entries in a directory ───────────────────────────────────── function Get-DirEntries { param([string]$DirPath) $entries = [System.Collections.Generic.List[object]]::new() try { $di = [System.IO.DirectoryInfo]::new($DirPath) foreach ($child in $di.EnumerateFileSystemInfos()) { $size = [long]0 $isDir = $child -is [System.IO.DirectoryInfo] if ($isDir) { $size = Get-DirectorySize -Path $child.FullName } else { $size = $child.Length } $age = '' try { $days = (New-TimeSpan -Start $child.LastWriteTime -End (Get-Date)).Days if ($days -gt 365) { $age = '>1yr' } elseif ($days -gt 180) { $age = '>6mo' } } catch {} $entries.Add([pscustomobject]@{ Name = $child.Name Path = $child.FullName Size = $size IsDir = $isDir Age = $age }) } } catch {} return ($entries | Sort-Object Size -Descending) } # ── JSON output mode ────────────────────────────────────────────────────────── if ($Json) { $entries = Get-DirEntries -DirPath $Path $total = ($entries | Measure-Object -Property Size -Sum).Sum $large = @($entries | Where-Object { $_.Size -gt 100MB } | Select-Object Name, Path, Size, IsDir) $output = [pscustomobject]@{ path = $Path entries = $entries | Select-Object Name, Path, Size, IsDir large_files = $large total_size = $total total_files = $entries.Count } $output | ConvertTo-Json -Depth 3 return } # ── Interactive TUI analyzer ────────────────────────────────────────────────── $stack = [System.Collections.Generic.Stack[string]]::new() $curPath = $Path $entries = $null $cursor = 0 $offset = 0 $maxVis = [Math]::Min(20, $Host.UI.RawUI.WindowSize.Height - 10) $largeMode = $false function Reload-Entries { $script:entries = @(Get-DirEntries -DirPath $curPath) $script:cursor = 0 $script:offset = 0 } Reload-Entries Hide-Cursor [Console]::TreatControlCAsInput = $false try { while ($true) { if (-not $entries -or $entries.Count -eq 0) { $entries = @() } $totalSize = ($entries | Measure-Object -Property Size -Sum).Sum $visibleEntries = if ($largeMode) { @($entries | Where-Object { $_.Size -gt 100MB }) } else { $entries } # ── Render ──────────────────────────────────────────────────────────── [Console]::Clear() $relPath = $curPath -replace [regex]::Escape($env:USERPROFILE), '~' Write-Host "" Write-SpectreHost " [bold deepskyblue1]WinMole Analyze[/] [grey]$(Esc $relPath)[/] [gold1]Total: $(Format-Bytes $totalSize)[/]" Write-Host "" if ($largeMode) { Write-SpectreHost " [gold1]Large files (>100 MB) — press L to return[/]" Write-Host "" } # ── Entry list ──────────────────────────────────────────────────────── $end = [Math]::Min($offset + $maxVis, $visibleEntries.Count) for ($i = $offset; $i -lt $end; $i++) { $e = $visibleEntries[$i] $isCur = ($i -eq $cursor) $pointer = if ($isCur) { '[deepskyblue1]▶[/]' } else { ' ' } $pct = if ($totalSize -gt 0) { $e.Size / $totalSize * 100 } else { 0 } $barColor = if ($pct -gt 50) { 'red' } elseif ($pct -gt 25) { 'yellow' } else { 'green' } $bar = Get-ProgressBar -Pct $pct -Width 18 -Color $barColor $icon = if ($e.IsDir) { '📁' } else { '��' } $ageStr = if ($e.Age) { " [grey]$($e.Age)[/]" } else { '' } $rank = '{0,3}.' -f ($i + 1) $sizePad = (Format-Bytes -Bytes $e.Size).PadLeft(8) $namePad = $e.Name if ($namePad.Length -gt 30) { $namePad = $namePad.Substring(0,27) + '...' } $namePad = $namePad.PadRight(30) Write-SpectreHost (" $pointer $rank $bar {0,5:F1}% | $icon [white]$(Esc $namePad)[/] [gold1]$sizePad[/]$ageStr" -f $pct) } # ── Footer ──────────────────────────────────────────────────────────── Write-Host "" Write-SpectreHost " [grey]↑↓ Navigate → Enter dir ← Back O Open F Reveal Del Delete L Large files Q Quit[/]" Write-Host "" # ── Input ───────────────────────────────────────────────────────────── $key = Read-Key switch ($key) { 'Up' { if ($cursor -gt 0) { $cursor-- if ($cursor -lt $offset) { $offset = $cursor } } } 'Down' { $maxCursor = $visibleEntries.Count - 1 if ($cursor -lt $maxCursor) { $cursor++ if ($cursor -ge $offset + $maxVis) { $offset++ } } } 'Right' { if ($cursor -lt $visibleEntries.Count) { $sel = $visibleEntries[$cursor] if ($sel.IsDir) { $stack.Push($curPath) $curPath = $sel.Path Reload-Entries $largeMode = $false } } } 'Enter' { if ($cursor -lt $visibleEntries.Count) { $sel = $visibleEntries[$cursor] if ($sel.IsDir) { $stack.Push($curPath) $curPath = $sel.Path Reload-Entries $largeMode = $false } } } 'Left' { if ($stack.Count -gt 0) { $curPath = $stack.Pop() Reload-Entries $largeMode = $false } } 'o' { if ($cursor -lt $visibleEntries.Count) { Start-Process -FilePath $visibleEntries[$cursor].Path -ErrorAction SilentlyContinue } } 'O' { if ($cursor -lt $visibleEntries.Count) { Start-Process -FilePath $visibleEntries[$cursor].Path -ErrorAction SilentlyContinue } } 'f' { if ($cursor -lt $visibleEntries.Count) { Start-Process -FilePath 'explorer.exe' -ArgumentList "/select,`"$($visibleEntries[$cursor].Path)`"" -ErrorAction SilentlyContinue } } 'F' { if ($cursor -lt $visibleEntries.Count) { Start-Process -FilePath 'explorer.exe' -ArgumentList "/select,`"$($visibleEntries[$cursor].Path)`"" -ErrorAction SilentlyContinue } } 'Delete' { if ($cursor -lt $visibleEntries.Count) { $sel = $visibleEntries[$cursor] Show-Cursor Write-Host "" Write-Host " Delete '$($sel.Name)' ($(Format-Bytes $sel.Size))? " -ForegroundColor Red -NoNewline Write-Host "(y/N) " -ForegroundColor DarkGray -NoNewline $conf = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown') Hide-Cursor if ($conf.Character -in @('y','Y')) { Remove-Item -LiteralPath $sel.Path -Recurse -Force -ErrorAction SilentlyContinue Write-OpLog "Analyze-delete: $($sel.Path) ($(Format-Bytes $sel.Size))" Reload-Entries $largeMode = $false } } } 'l' { $largeMode = -not $largeMode $cursor = 0 $offset = 0 } 'L' { $largeMode = -not $largeMode $cursor = 0 $offset = 0 } 'Escape' { break } } if ($key -eq 'Escape') { break } } } finally { Show-Cursor [Console]::TreatControlCAsInput = $false } Write-Host "" |