Merc.psm1
|
function Invoke-Merc { <# .SYNOPSIS Interactively kill processes using fzf, with window titles for context. .DESCRIPTION Lists all processes with their start time, PID, name, and associated window titles. Uses fzf for fuzzy selection (multi-select with Tab). Selected processes are killed. Requires fzf to be installed and available in PATH. .PARAMETER IncludeJunk Include junk/system windows (Default IME, MSCTFIME UI, etc.) in the display. .EXAMPLE Invoke-Merc # Then type "teams" or "gluk" to filter, Tab to select multiple, Enter to kill .EXAMPLE Invoke-Merc -IncludeJunk # Include junk/system windows in the list .EXAMPLE merc -j # Using the alias with short parameter .LINK https://github.com/junegunn/fzf #> [CmdletBinding()] [Alias('merc')] param( [Alias('j', 'Junk')] [switch]$IncludeJunk ) # Use Win32 API to enumerate all visible windows Add-Type -ErrorAction SilentlyContinue @" using System; using System.Collections.Generic; using System.Runtime.InteropServices; using System.Text; public class MercWindowEnumerator { [DllImport("user32.dll")] private static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam); [DllImport("user32.dll")] private static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount); [DllImport("user32.dll")] private static extern int GetWindowTextLength(IntPtr hWnd); [DllImport("user32.dll")] private static extern bool IsWindowVisible(IntPtr hWnd); [DllImport("user32.dll")] private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId); private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam); public static Dictionary<uint, List<string>> GetWindowsByProcess() { var result = new Dictionary<uint, List<string>>(); EnumWindows((hWnd, lParam) => { if (IsWindowVisible(hWnd)) { int length = GetWindowTextLength(hWnd); if (length > 0) { StringBuilder sb = new StringBuilder(length + 1); GetWindowText(hWnd, sb, sb.Capacity); string title = sb.ToString(); if (!string.IsNullOrWhiteSpace(title)) { uint pid; GetWindowThreadProcessId(hWnd, out pid); if (!result.ContainsKey(pid)) { result[pid] = new List<string>(); } result[pid].Add(title); } } } return true; }, IntPtr.Zero); return result; } } "@ # Get all windows grouped by PID $windowsByPid = [MercWindowEnumerator]::GetWindowsByProcess() # Junk window titles to filter out $junkTitles = @( 'Default IME', 'MSCTFIME UI', 'DesktopWindowXamlSource', 'Program Manager', 'Battery Meter' ) Get-Process | Where-Object { $null -ne $_.StartTime } | ForEach-Object { $proc = $_ $windows = "" # Look up windows for this PID if ($windowsByPid.ContainsKey([uint32]$proc.Id)) { $titles = $windowsByPid[[uint32]$proc.Id] | Where-Object { $_ -and ($IncludeJunk -or ($_ -notin $junkTitles)) } | Select-Object -Unique $windows = $titles -join " | " } # Truncate long window titles if ($windows.Length -gt 80) { $windows = $windows.Substring(0, 77) + "..." } [PSCustomObject]@{ StartTime = $proc.StartTime Id = $proc.Id ProcessName = $proc.ProcessName Windows = $windows } } | Sort-Object StartTime | ForEach-Object { $time = $_.StartTime.ToString("yyyy-MM-dd HH:mm:ss") $display = "{0} {1,6} {2,-25} {3}" -f $time, $_.Id, $_.ProcessName, $_.Windows # Embed the PID at the start with a delimiter for easy extraction "$($_.Id)`t$display" } | fzf -m --with-nth=2.. --delimiter="`t" --header="Select processes to kill (Tab=multi, Enter=confirm)" | ForEach-Object { $procId = $_.Split("`t")[0] $procName = (Get-Process -Id $procId -ErrorAction SilentlyContinue).ProcessName Write-Host "Killing $procName (PID $procId)..." -ForegroundColor Yellow Stop-Process -Id $procId -Force -ErrorAction SilentlyContinue } } |