|
| 1 | +<# |
| 2 | + OpenCoperLock - quick declutter for the Windows 11 right-click menu. |
| 3 | +
|
| 4 | + Disables the noisy app entries you named (Edit with Photos, Create with Designer, Edit with Paint, |
| 5 | + Ask Copilot) - matched by keyword so it works in French or English. It scans the places these |
| 6 | + live: classic verbs and shell-extension handlers under the file/folder roots AND under |
| 7 | + SystemFileAssociations\image\... (where "Edit with Photos/Paint" hide). |
| 8 | +
|
| 9 | + NOTHING is deleted - verbs get a reversible `LegacyDisable` flag and shell extensions go on |
| 10 | + Windows' official "Blocked" list. Re-enable later with -Restore (same keywords) or the GUI manager. |
| 11 | +
|
| 12 | + Usage: |
| 13 | + windows-declutter.cmd # disable the default noisy set (asks to confirm) |
| 14 | + ...ps1 -List # just show what matches, change nothing |
| 15 | + ...ps1 -Keywords Photos,Paint # target your own keywords |
| 16 | + ...ps1 -Restore # re-enable everything matching the keywords |
| 17 | +#> |
| 18 | +param( |
| 19 | + [string[]]$Keywords = @('Photos', 'Designer', 'Paint', 'Copilot'), |
| 20 | + [switch]$List, |
| 21 | + [switch]$Restore |
| 22 | +) |
| 23 | +$ErrorActionPreference = 'Stop' |
| 24 | +$BlockedKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Shell Extensions\Blocked' |
| 25 | + |
| 26 | +function Test-Admin { |
| 27 | + $id = [Security.Principal.WindowsIdentity]::GetCurrent() |
| 28 | + (New-Object Security.Principal.WindowsPrincipal($id)).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator) |
| 29 | +} |
| 30 | +# Self-elevate: most of these entries are system-wide (HKLM). |
| 31 | +if (-not (Test-Admin) -and -not $List -and $PSCommandPath) { |
| 32 | + $a = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', "`"$PSCommandPath`"", '-Keywords', ($Keywords -join ',')) |
| 33 | + if ($Restore) { $a += '-Restore' } |
| 34 | + try { Start-Process powershell -Verb RunAs -ArgumentList $a; exit } catch { } |
| 35 | +} |
| 36 | +$IsAdmin = Test-Admin |
| 37 | + |
| 38 | +$imgExt = @('.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.heic', '.tif', '.tiff', '.ico', '.avif') |
| 39 | +$verbRoots = @('*\shell', 'AllFilesystemObjects\shell', 'Directory\shell', 'Directory\Background\shell', |
| 40 | + 'Folder\shell', 'SystemFileAssociations\image\shell', 'SystemFileAssociations\text\shell') + |
| 41 | + ($imgExt | ForEach-Object { "SystemFileAssociations\$_\shell" }) |
| 42 | +$shexRoots = @('*', 'AllFilesystemObjects', 'Directory', 'Directory\Background', 'Folder', 'SystemFileAssociations\image') |
| 43 | +$hives = [ordered]@{ 'HKCU' = 'HKCU:\Software\Classes'; 'HKLM' = 'HKLM:\SOFTWARE\Classes' } |
| 44 | + |
| 45 | +function Matches($text) { |
| 46 | + foreach ($kw in $Keywords) { if ("$text" -match [regex]::Escape($kw)) { return $true } } |
| 47 | + return $false |
| 48 | +} |
| 49 | + |
| 50 | +function Find-Candidates { |
| 51 | + $out = New-Object System.Collections.ArrayList |
| 52 | + foreach ($h in $hives.GetEnumerator()) { |
| 53 | + foreach ($vr in $verbRoots) { |
| 54 | + $base = Join-Path $h.Value $vr |
| 55 | + if (-not (Test-Path -LiteralPath $base)) { continue } |
| 56 | + foreach ($k in Get-ChildItem -LiteralPath $base -ErrorAction SilentlyContinue) { |
| 57 | + $props = Get-ItemProperty -LiteralPath $k.PSPath -ErrorAction SilentlyContinue |
| 58 | + $name = $props.MUIVerb; if (-not $name) { $name = $props.'(default)' }; if (-not $name) { $name = $k.PSChildName } |
| 59 | + if ((Matches $name) -or (Matches $k.PSChildName)) { |
| 60 | + [void]$out.Add([pscustomobject]@{ Type = 'verb'; Scope = $h.Key; Name = "$name"; Where = $vr |
| 61 | + Path = $k.PSPath; Clsid = ''; Disabled = ($props.PSObject.Properties.Name -contains 'LegacyDisable') }) |
| 62 | + } |
| 63 | + } |
| 64 | + } |
| 65 | + foreach ($sr in $shexRoots) { |
| 66 | + $base = Join-Path $h.Value "$sr\shellex\ContextMenuHandlers" |
| 67 | + if (-not (Test-Path -LiteralPath $base)) { continue } |
| 68 | + foreach ($k in Get-ChildItem -LiteralPath $base -ErrorAction SilentlyContinue) { |
| 69 | + $clsid = (Get-ItemProperty -LiteralPath $k.PSPath -ErrorAction SilentlyContinue).'(default)' |
| 70 | + if (-not $clsid) { $clsid = $k.PSChildName } |
| 71 | + $friendly = $k.PSChildName |
| 72 | + if ($clsid -match '^{.+}$') { |
| 73 | + $cn = (Get-ItemProperty -LiteralPath "HKLM:\SOFTWARE\Classes\CLSID\$clsid" -ErrorAction SilentlyContinue).'(default)' |
| 74 | + if ($cn) { $friendly = $cn } |
| 75 | + } |
| 76 | + if ((Matches $friendly) -or (Matches $k.PSChildName)) { |
| 77 | + $blocked = $false |
| 78 | + if ($clsid -match '^{.+}$' -and (Test-Path -LiteralPath $BlockedKey)) { |
| 79 | + $blocked = ($null -ne (Get-ItemProperty -LiteralPath $BlockedKey -ErrorAction SilentlyContinue).$clsid) |
| 80 | + } |
| 81 | + [void]$out.Add([pscustomobject]@{ Type = 'shellex'; Scope = $h.Key; Name = "$friendly"; Where = $sr |
| 82 | + Path = $k.PSPath; Clsid = "$clsid"; Disabled = $blocked }) |
| 83 | + } |
| 84 | + } |
| 85 | + } |
| 86 | + } |
| 87 | + $out | Sort-Object Name, Where -Unique |
| 88 | +} |
| 89 | + |
| 90 | +function Apply($e, $disable) { |
| 91 | + if ($e.Scope -eq 'HKLM' -and -not $IsAdmin) { return "needs admin" } |
| 92 | + try { |
| 93 | + if ($e.Type -eq 'verb') { |
| 94 | + if ($disable) { New-ItemProperty -LiteralPath $e.Path -Name 'LegacyDisable' -Value '' -PropertyType String -Force | Out-Null } |
| 95 | + else { Remove-ItemProperty -LiteralPath $e.Path -Name 'LegacyDisable' -ErrorAction SilentlyContinue } |
| 96 | + } else { |
| 97 | + if ($e.Clsid -notmatch '^{.+}$') { return "no CLSID" } |
| 98 | + if (-not (Test-Path $BlockedKey)) { New-Item $BlockedKey -Force | Out-Null } |
| 99 | + if ($disable) { New-ItemProperty $BlockedKey -Name $e.Clsid -Value $e.Name -PropertyType String -Force | Out-Null } |
| 100 | + else { Remove-ItemProperty $BlockedKey -Name $e.Clsid -ErrorAction SilentlyContinue } |
| 101 | + } |
| 102 | + return $null |
| 103 | + } catch { return $_.Exception.Message } |
| 104 | +} |
| 105 | + |
| 106 | +Write-Host "OpenCoperLock declutter - keywords: $($Keywords -join ', ')" -ForegroundColor Cyan |
| 107 | +$cands = @(Find-Candidates) |
| 108 | +if ($cands.Count -eq 0) { Write-Host "Nothing matched. (These entries may be part of Windows' modern menu, not the classic registry.)" -ForegroundColor Yellow; if (-not $PSCommandPath) { Read-Host "Enter to close" }; return } |
| 109 | + |
| 110 | +Write-Host "" |
| 111 | +$i = 0 |
| 112 | +foreach ($e in $cands) { |
| 113 | + $i++ |
| 114 | + $state = if ($e.Disabled) { '[already OFF]' } else { '[on]' } |
| 115 | + Write-Host (" {0,2}. {1,-12} {2,-6} {3,-34} {4} ({5})" -f $i, $state, $e.Scope, $e.Name, $e.Where, $e.Type) |
| 116 | +} |
| 117 | +Write-Host "" |
| 118 | + |
| 119 | +if ($List) { return } |
| 120 | + |
| 121 | +$verb = if ($Restore) { 're-enable' } else { 'disable' } |
| 122 | +$ans = (Read-Host "$verb the $($cands.Count) matching entries above? [Y/n]").Trim().ToLower() |
| 123 | +if ($ans -eq 'n' -or $ans -eq 'no' -or $ans -eq 'non') { Write-Host "Cancelled." -ForegroundColor DarkGray; return } |
| 124 | + |
| 125 | +$done = 0; $skip = 0 |
| 126 | +foreach ($e in $cands) { |
| 127 | + $err = Apply $e (-not $Restore) |
| 128 | + if ($err) { Write-Host (" ! {0}: {1}" -f $e.Name, $err) -ForegroundColor Yellow; $skip++ } |
| 129 | + else { $done++ } |
| 130 | +} |
| 131 | +Write-Host "" |
| 132 | +Write-Host ("{0} {1}d, {2} skipped." -f $done, $verb, $skip) -ForegroundColor Green |
| 133 | +Write-Host "Restart Explorer to see the change: taskkill /f /im explorer.exe & start explorer.exe" -ForegroundColor DarkGray |
| 134 | +if (-not $PSCommandPath) { Read-Host "Enter to close" } |
0 commit comments