Compare commits

...
7 Commits
Author SHA1 Message Date
ChrisTitusTechandgithub-actions[bot] 15b31634e3 Update sponsors in README 2026-07-14 15:58:08 +00:00
Paweł GwozdowskiandGitHub 157a66b14d Update Invoke-WPFUIElements.ps1 (#4824) 2026-07-13 13:20:53 -05:00
9791386f53 fix: resolve silent failures in AppX removal and PS7 DISM bugs (#4822)
* fix: resolve silent failures in AppX removal and PS7 DISM bugs

- Fixed a bug where Remove-AppxPackage failed silently in PowerShell 7 due to pipeline binding issues. We now explicitly loop through Get-AppxPackage results and pass PackageFullName directly.
- Added wildcard matching to Get-AppxPackage and Get-AppxProvisionedPackage to reliably locate packages regardless of short name vs family name variations.
- Offloaded Get-AppxProvisionedPackage and Remove-AppxProvisionedPackage execution to Windows PowerShell 5.1 (powershell.exe) to bypass PowerShell 7 DISM COM exceptions ("Class not registered") and performance hangs.
- Deduplicated AppX removal logic by calling Remove-WinUtilAPPX directly from Invoke-WPFAppxRemoval instead of copying the implementation.

* refactor(Remove-WinUtilAPPX): use script block to improve readability

* feat: enhance AppX removal process and add provisioned package removal function

* fix(tests): update AppX removal tests

This also adds tests for the new Remove-WinUtilProvisionedAPPX functionality

* Move AppX removal under Tweaks

* Address AppX removal review feedback

* Complete AppX removal error handling

* Prevent AppX cleanup confirmation prompts

---------

Co-authored-by: Chris Titus <contact@christitus.com>
2026-07-13 13:16:35 -05:00
Chris TitusandGitHub c8e756dc11 chore: Update generated dev docs (#4838) 2026-07-13 11:40:20 -05:00
Sean (ANGRYxScotsman)andGitHub 9e21c8f4a7 Updated ConsumerFeatures description and small dev doc edit (#4817)
* Update tweaks.json

* Update _index.md

* Update _index.md
2026-07-13 11:39:49 -05:00
Chris TitusandGitHub 8cd8679512 chore: Update generated dev docs (#4837) 2026-07-13 11:37:52 -05:00
ac11c73b64 feat(legacy-panels): add Firewall, Security, Programs, and Mouse to L… (#4809)
* feat(legacy-panels): add Firewall, Security, Programs, and Mouse to Legacy Windows Panels

- Added `firewall.cpl`, `wscui.cpl`, `appwiz.cpl`, and `main.cpl` to `config/feature.json`

* chore(legacy-panels): alphabetize panel entries

---------

Co-authored-by: Chris Titus <contact@christitus.com>
2026-07-13 11:37:22 -05:00
30 changed files with 367 additions and 61 deletions
+1 -1
View File
@@ -77,7 +77,7 @@ See https://github.com/ChrisTitusTech/winutil/blob/main/.github/CONTRIBUTING.md
These are the sponsors that help keep this project alive with monthly contributions.
<!-- sponsors --><a href="https://github.com/ysaito8015"><img src="https:&#x2F;&#x2F;github.com&#x2F;ysaito8015.png" width="60px" alt="User avatar: Yusuke Saito" /></a><a href="https://github.com/dwelfusius"><img src="https:&#x2F;&#x2F;github.com&#x2F;dwelfusius.png" width="60px" alt="User avatar: " /></a><a href="https://github.com/mews-se"><img src="https:&#x2F;&#x2F;github.com&#x2F;mews-se.png" width="60px" alt="User avatar: Martin Stockzell" /></a><a href="https://github.com/jdiegmueller"><img src="https:&#x2F;&#x2F;github.com&#x2F;jdiegmueller.png" width="60px" alt="User avatar: Jason A. Diegmueller" /></a><a href="https://github.com/robertsandrock"><img src="https:&#x2F;&#x2F;github.com&#x2F;robertsandrock.png" width="60px" alt="User avatar: RMS" /></a><a href="https://github.com/paulsheets"><img src="https:&#x2F;&#x2F;github.com&#x2F;paulsheets.png" width="60px" alt="User avatar: Paul" /></a><a href="https://github.com/djones369"><img src="https:&#x2F;&#x2F;github.com&#x2F;djones369.png" width="60px" alt="User avatar: Dave J (WhamGeek)" /></a><a href="https://github.com/anthonymendez"><img src="https:&#x2F;&#x2F;github.com&#x2F;anthonymendez.png" width="60px" alt="User avatar: Anthony Mendez" /></a><a href="https://github.com/FatBastard0"><img src="https:&#x2F;&#x2F;github.com&#x2F;FatBastard0.png" width="60px" alt="User avatar: " /></a><a href="https://github.com/DursleyGuy"><img src="https:&#x2F;&#x2F;github.com&#x2F;DursleyGuy.png" width="60px" alt="User avatar: DursleyGuy" /></a><a href="https://github.com/DwayneTheRockLobster1"><img src="https:&#x2F;&#x2F;github.com&#x2F;DwayneTheRockLobster1.png" width="60px" alt="User avatar: " /></a><a href="https://github.com/KieraKujisawa"><img src="https:&#x2F;&#x2F;github.com&#x2F;KieraKujisawa.png" width="60px" alt="User avatar: Kiera Meredith" /></a><a href="https://github.com/andrewpayne68"><img src="https:&#x2F;&#x2F;github.com&#x2F;andrewpayne68.png" width="60px" alt="User avatar: Andrew P" /></a><a href="https://github.com/johanwildeboer"><img src="https:&#x2F;&#x2F;github.com&#x2F;johanwildeboer.png" width="60px" alt="User avatar: " /></a><a href="https://github.com/lukas346"><img src="https:&#x2F;&#x2F;github.com&#x2F;lukas346.png" width="60px" alt="User avatar: Wook" /></a><a href="https://github.com/tsv31"><img src="https:&#x2F;&#x2F;github.com&#x2F;tsv31.png" width="60px" alt="User avatar: Sorin" /></a><!-- sponsors -->
<!-- sponsors --><a href="https://github.com/ysaito8015"><img src="https:&#x2F;&#x2F;github.com&#x2F;ysaito8015.png" width="60px" alt="User avatar: Yusuke Saito" /></a><a href="https://github.com/dwelfusius"><img src="https:&#x2F;&#x2F;github.com&#x2F;dwelfusius.png" width="60px" alt="User avatar: " /></a><a href="https://github.com/mews-se"><img src="https:&#x2F;&#x2F;github.com&#x2F;mews-se.png" width="60px" alt="User avatar: Martin Stockzell" /></a><a href="https://github.com/jdiegmueller"><img src="https:&#x2F;&#x2F;github.com&#x2F;jdiegmueller.png" width="60px" alt="User avatar: Jason A. Diegmueller" /></a><a href="https://github.com/robertsandrock"><img src="https:&#x2F;&#x2F;github.com&#x2F;robertsandrock.png" width="60px" alt="User avatar: RMS" /></a><a href="https://github.com/paulsheets"><img src="https:&#x2F;&#x2F;github.com&#x2F;paulsheets.png" width="60px" alt="User avatar: Paul" /></a><a href="https://github.com/djones369"><img src="https:&#x2F;&#x2F;github.com&#x2F;djones369.png" width="60px" alt="User avatar: Dave J (WhamGeek)" /></a><a href="https://github.com/anthonymendez"><img src="https:&#x2F;&#x2F;github.com&#x2F;anthonymendez.png" width="60px" alt="User avatar: Anthony Mendez" /></a><a href="https://github.com/FatBastard0"><img src="https:&#x2F;&#x2F;github.com&#x2F;FatBastard0.png" width="60px" alt="User avatar: " /></a><a href="https://github.com/DursleyGuy"><img src="https:&#x2F;&#x2F;github.com&#x2F;DursleyGuy.png" width="60px" alt="User avatar: DursleyGuy" /></a><a href="https://github.com/DwayneTheRockLobster1"><img src="https:&#x2F;&#x2F;github.com&#x2F;DwayneTheRockLobster1.png" width="60px" alt="User avatar: " /></a><a href="https://github.com/KieraKujisawa"><img src="https:&#x2F;&#x2F;github.com&#x2F;KieraKujisawa.png" width="60px" alt="User avatar: Kiera Meredith" /></a><a href="https://github.com/andrewpayne68"><img src="https:&#x2F;&#x2F;github.com&#x2F;andrewpayne68.png" width="60px" alt="User avatar: Andrew P" /></a><a href="https://github.com/johanwildeboer"><img src="https:&#x2F;&#x2F;github.com&#x2F;johanwildeboer.png" width="60px" alt="User avatar: " /></a><a href="https://github.com/lukas346"><img src="https:&#x2F;&#x2F;github.com&#x2F;lukas346.png" width="60px" alt="User avatar: Wook" /></a><a href="https://github.com/tsv31"><img src="https:&#x2F;&#x2F;github.com&#x2F;tsv31.png" width="60px" alt="User avatar: Sorin" /></a><a href="https://github.com/seanh1995"><img src="https:&#x2F;&#x2F;github.com&#x2F;seanh1995.png" width="60px" alt="User avatar: Sean (ANGRYxScotsman)" /></a><!-- sponsors -->
---
+52 -8
View File
@@ -179,6 +179,17 @@
"function": "Invoke-WPFFixesWinget",
"link": "https://winutil.christitus.com/dev/features/fixes/winget"
},
"WPFPanelComputer": {
"Content": "Computer Management",
"category": "Legacy Windows Panels",
"panel": "2",
"Type": "Button",
"ButtonWidth": "300",
"InvokeScript": [
"compmgmt.msc"
],
"link": "https://winutil.christitus.com/dev/features/legacy-windows-panels/computer"
},
"WPFPanelControl": {
"Content": "Control Panel",
"category": "Legacy Windows Panels",
@@ -190,16 +201,16 @@
],
"link": "https://winutil.christitus.com/dev/features/legacy-windows-panels/control"
},
"WPFPanelComputer": {
"Content": "Computer Management",
"WPFPanelMouse": {
"Content": "Mouse Properties",
"category": "Legacy Windows Panels",
"panel": "2",
"Type": "Button",
"ButtonWidth": "300",
"InvokeScript": [
"compmgmt.msc"
"main.cpl"
],
"link": "https://winutil.christitus.com/dev/features/legacy-windows-panels/computer"
"link": "https://winutil.christitus.com/dev/features/legacy-windows-panels/mouse"
},
"WPFPanelNetwork": {
"Content": "Network Connections",
@@ -234,6 +245,17 @@
],
"link": "https://winutil.christitus.com/dev/features/legacy-windows-panels/printer"
},
"WPFPanelPrograms": {
"Content": "Programs and Features",
"category": "Legacy Windows Panels",
"panel": "2",
"Type": "Button",
"ButtonWidth": "300",
"InvokeScript": [
"appwiz.cpl"
],
"link": "https://winutil.christitus.com/dev/features/legacy-windows-panels/programs"
},
"WPFPanelRegion": {
"Content": "Region",
"category": "Legacy Windows Panels",
@@ -245,16 +267,16 @@
],
"link": "https://winutil.christitus.com/dev/features/legacy-windows-panels/region"
},
"WPFPanelRestore": {
"Content": "Windows Restore",
"WPFPanelSecurity": {
"Content": "Security and Maintenance",
"category": "Legacy Windows Panels",
"panel": "2",
"Type": "Button",
"ButtonWidth": "300",
"InvokeScript": [
"rstrui.exe"
"wscui.cpl"
],
"link": "https://winutil.christitus.com/dev/features/legacy-windows-panels/restore"
"link": "https://winutil.christitus.com/dev/features/legacy-windows-panels/security"
},
"WPFPanelSound": {
"Content": "Sound Settings",
@@ -289,6 +311,28 @@
],
"link": "https://winutil.christitus.com/dev/features/legacy-windows-panels/timedate"
},
"WPFPanelFirewall": {
"Content": "Windows Defender Firewall",
"category": "Legacy Windows Panels",
"panel": "2",
"Type": "Button",
"ButtonWidth": "300",
"InvokeScript": [
"firewall.cpl"
],
"link": "https://winutil.christitus.com/dev/features/legacy-windows-panels/firewall"
},
"WPFPanelRestore": {
"Content": "Windows Restore",
"category": "Legacy Windows Panels",
"panel": "2",
"Type": "Button",
"ButtonWidth": "300",
"InvokeScript": [
"rstrui.exe"
],
"link": "https://winutil.christitus.com/dev/features/legacy-windows-panels/restore"
},
"WPFWinUtilInstallPSProfile": {
"Content": "CTT PowerShell Profile - Install",
"category": "Powershell Profile Powershell 7+ Only",
+1 -1
View File
@@ -429,7 +429,7 @@
},
"WPFTweaksConsumerFeatures": {
"Content": "ConsumerFeatures - Disable",
"Description": "Windows will not automatically install any games, third-party apps, or application links from the Windows Store for the signed-in user. Some default Apps will be inaccessible (e.g. Phone Link).",
"Description": "Stops promoted app installs and reduces app suggestions from Microsoft Store content.",
"category": "Essential Tweaks",
"panel": "1",
"registry": [
+1 -1
View File
@@ -5,7 +5,7 @@ toc: false
---
> [!NOTE]
> This section contains technical documentation for developers. For end-user documentation, see the [User Guide](../userguide/).
> This section contains code-only references. For end-user documentation, see the [User Guide](../userguide/).
## Architecture
@@ -3,7 +3,7 @@ title: "Computer Management"
description: ""
---
```json {filename="config/feature.json",linenos=inline,linenostart=193}
```json {filename="config/feature.json",linenos=inline,linenostart=182}
"WPFPanelComputer": {
"Content": "Computer Management",
"category": "Legacy Windows Panels",
@@ -3,7 +3,7 @@ title: "Control Panel"
description: ""
---
```json {filename="config/feature.json",linenos=inline,linenostart=182}
```json {filename="config/feature.json",linenos=inline,linenostart=193}
"WPFPanelControl": {
"Content": "Control Panel",
"category": "Legacy Windows Panels",
@@ -0,0 +1,16 @@
---
title: "Windows Defender Firewall"
description: ""
---
```json {filename="config/feature.json",linenos=inline,linenostart=314}
"WPFPanelFirewall": {
"Content": "Windows Defender Firewall",
"category": "Legacy Windows Panels",
"panel": "2",
"Type": "Button",
"ButtonWidth": "300",
"InvokeScript": [
"firewall.cpl"
],
```
@@ -0,0 +1,16 @@
---
title: "Mouse Properties"
description: ""
---
```json {filename="config/feature.json",linenos=inline,linenostart=204}
"WPFPanelMouse": {
"Content": "Mouse Properties",
"category": "Legacy Windows Panels",
"panel": "2",
"Type": "Button",
"ButtonWidth": "300",
"InvokeScript": [
"main.cpl"
],
```
@@ -3,7 +3,7 @@ title: "Network Connections"
description: ""
---
```json {filename="config/feature.json",linenos=inline,linenostart=204}
```json {filename="config/feature.json",linenos=inline,linenostart=215}
"WPFPanelNetwork": {
"Content": "Network Connections",
"category": "Legacy Windows Panels",
@@ -3,7 +3,7 @@ title: "Power Panel"
description: ""
---
```json {filename="config/feature.json",linenos=inline,linenostart=215}
```json {filename="config/feature.json",linenos=inline,linenostart=226}
"WPFPanelPower": {
"Content": "Power Panel",
"category": "Legacy Windows Panels",
@@ -3,7 +3,7 @@ title: "Printer Panel"
description: ""
---
```json {filename="config/feature.json",linenos=inline,linenostart=226}
```json {filename="config/feature.json",linenos=inline,linenostart=237}
"WPFPanelPrinter": {
"Content": "Printer Panel",
"category": "Legacy Windows Panels",
@@ -0,0 +1,16 @@
---
title: "Programs and Features"
description: ""
---
```json {filename="config/feature.json",linenos=inline,linenostart=248}
"WPFPanelPrograms": {
"Content": "Programs and Features",
"category": "Legacy Windows Panels",
"panel": "2",
"Type": "Button",
"ButtonWidth": "300",
"InvokeScript": [
"appwiz.cpl"
],
```
@@ -3,7 +3,7 @@ title: "Region"
description: ""
---
```json {filename="config/feature.json",linenos=inline,linenostart=237}
```json {filename="config/feature.json",linenos=inline,linenostart=259}
"WPFPanelRegion": {
"Content": "Region",
"category": "Legacy Windows Panels",
@@ -3,7 +3,7 @@ title: "Windows Restore"
description: ""
---
```json {filename="config/feature.json",linenos=inline,linenostart=248}
```json {filename="config/feature.json",linenos=inline,linenostart=325}
"WPFPanelRestore": {
"Content": "Windows Restore",
"category": "Legacy Windows Panels",
@@ -0,0 +1,16 @@
---
title: "Security and Maintenance"
description: ""
---
```json {filename="config/feature.json",linenos=inline,linenostart=270}
"WPFPanelSecurity": {
"Content": "Security and Maintenance",
"category": "Legacy Windows Panels",
"panel": "2",
"Type": "Button",
"ButtonWidth": "300",
"InvokeScript": [
"wscui.cpl"
],
```
@@ -3,7 +3,7 @@ title: "Sound Settings"
description: ""
---
```json {filename="config/feature.json",linenos=inline,linenostart=259}
```json {filename="config/feature.json",linenos=inline,linenostart=281}
"WPFPanelSound": {
"Content": "Sound Settings",
"category": "Legacy Windows Panels",
@@ -3,7 +3,7 @@ title: "System Properties"
description: ""
---
```json {filename="config/feature.json",linenos=inline,linenostart=270}
```json {filename="config/feature.json",linenos=inline,linenostart=292}
"WPFPanelSystem": {
"Content": "System Properties",
"category": "Legacy Windows Panels",
@@ -3,7 +3,7 @@ title: "Time and Date"
description: ""
---
```json {filename="config/feature.json",linenos=inline,linenostart=281}
```json {filename="config/feature.json",linenos=inline,linenostart=303}
"WPFPanelTimedate": {
"Content": "Time and Date",
"category": "Legacy Windows Panels",
@@ -6,7 +6,7 @@ description: ""
```json {filename="config/tweaks.json",linenos=inline,linenostart=430}
"WPFTweaksConsumerFeatures": {
"Content": "ConsumerFeatures - Disable",
"Description": "Windows will not automatically install any games, third-party apps, or application links from the Windows Store for the signed-in user. Some default Apps will be inaccessible (e.g. Phone Link).",
"Description": "Stops promoted app installs and reduces app suggestions from Microsoft Store content.",
"category": "Essential Tweaks",
"panel": "1",
"registry": [
@@ -78,6 +78,7 @@ function Invoke-WinUtilTweaks {
$sync.configs.tweaks.$CheckBox.appx | ForEach-Object {
Remove-WinUtilAPPX -Name $psitem
}
Remove-WinUtilProvisionedAPPX -PackageList $sync.configs.tweaks.$CheckBox.appx
}
}
Write-WinUtilLog -Component "Tweaks" -Message "$action tweak completed: $CheckBox"
+15 -2
View File
@@ -17,7 +17,20 @@ function Remove-WinUtilAPPX {
Write-Host "Removing $Name"
Write-WinUtilLog -Component "AppX" -Message "Removing AppX package pattern: $Name"
Get-AppxPackage $Name -AllUsers | Remove-AppxPackage -AllUsers
Get-AppxProvisionedPackage -Online | Where-Object DisplayName -like $Name | Remove-AppxProvisionedPackage -Online
# We explicitly loop through packages instead of using the pipeline because PowerShell 7 pipeline binding
# for Remove-AppxPackage fails silently, and Get-AppxPackage -AllUsers returns duplicate objects for each user profile.
$pkgs = Get-AppxPackage "*$Name*" -AllUsers | Sort-Object -Property PackageFullName -Unique
if ($null -ne $pkgs) {
foreach ($pkg in $pkgs) {
try {
Remove-AppxPackage -Package $pkg.PackageFullName -AllUsers -ErrorAction Stop
}
catch {
Write-WinUtilLog -Level "ERROR" -Component "AppX" -Message "Failed to remove AppX package $($pkg.PackageFullName): $($_.Exception.Message)"
}
}
}
Write-WinUtilLog -Component "AppX" -Message "AppX removal completed for package pattern: $Name"
}
@@ -0,0 +1,61 @@
function Remove-WinUtilProvisionedAPPX {
<#
.SYNOPSIS
Removes all AppX provisioned packages that match the given names
.PARAMETER PackageList
An array of names of the APPX packages to remove
.EXAMPLE
Remove-WinUtilProvisionedAPPX -PackageList @("Microsoft.Microsoft3DViewer", "Microsoft.WindowsCalculator")
#>
param (
[string[]]$PackageList
)
if ($null -eq $PackageList -or $PackageList.Count -eq 0) {
return
}
Write-Host "`nRemoving provisioned packages..."
Write-WinUtilLog -Component "AppX" -Message "Removing AppX provisioned packages: $($PackageList -join ', ')"
# DISM cmdlets like Get-AppxProvisionedPackage often fail with "Class not registered" or hang in PowerShell 7.
# We shell out to Windows PowerShell 5.1 (powershell.exe) to reliably remove the provisioned packages.
$ps5Command = {
$pkgs = $args
$provisionedPackages = Get-AppxProvisionedPackage -Online -ErrorAction SilentlyContinue
$failures = [System.Collections.Generic.List[string]]::new()
foreach ($Package in $pkgs) {
$provs = $provisionedPackages |
Where-Object DisplayName -Like "*$Package*"
if ($null -ne $provs) {
foreach ($prov in $provs) {
try {
Remove-AppxProvisionedPackage -Online -PackageName $prov.PackageName -ErrorAction Stop | Out-Null
}
catch {
$failures.Add("Failed to remove provisioned AppX package $($prov.PackageName): $($_.Exception.Message)")
}
}
}
}
if ($failures.Count -gt 0) {
throw ($failures -join [Environment]::NewLine)
}
}
$removalOutput = powershell.exe -NoProfile -NonInteractive -Command $ps5Command -args $PackageList 2>&1
if ($LASTEXITCODE -ne 0 -or $null -ne $removalOutput) {
$failureDetails = ($removalOutput | Out-String).Trim()
Write-WinUtilLog -Level "ERROR" -Component "AppX" -Message "AppX provisioned package removal failed: $failureDetails"
return
}
Write-WinUtilLog -Component "AppX" -Message "AppX provisioned package removal completed."
}
+10 -4
View File
@@ -13,11 +13,13 @@ function Invoke-WPFAppxRemoval {
$sync.ProcessRunning = $true
Write-WinUtilLog -Component "AppX" -Message "Starting AppX removal for $(@($selected).Count) selected package(s)."
$packageList = [System.Collections.Generic.List[string]]::new()
foreach ($key in $selected) {
if ($key -eq "WPFAppxMicrosoft_XboxGamingOverlay") {
# Making sure Game Bar isn't running
Write-WinUtilLog -Component "AppX" -Message "Stopping GameBarFTServer before removing Xbox Gaming Overlay."
Stop-Process -Name GameBarFTServer
Stop-Process -Name GameBarFTServer -Force -Confirm:$false -ErrorAction SilentlyContinue
# This stops annoying ms-gamebar popup when launching games.
Write-WinUtilLog -Component "AppX" -Message "Disabling Game DVR capture before removing Xbox Gaming Overlay."
@@ -25,14 +27,14 @@ function Invoke-WPFAppxRemoval {
}
if ($key -eq "WPFAppxMicrosoft_WindowsNotepad") {
# i hope your having fun reading this
Write-WinUtilLog -Component "AppX" -Message "Stopping dllhost before removing Notepad."
Stop-Process -Name dllhost
Stop-Process -Name dllhost -Force -Confirm:$false -ErrorAction SilentlyContinue
}
Write-Host "Removing $($apps[$key].Content)"
Write-WinUtilLog -Component "AppX" -Message "Removing $($apps[$key].Content) ($($apps[$key].PackageId))."
Get-AppxPackage -Name $apps[$key].PackageId -AllUsers | Remove-AppxPackage -AllUsers
Remove-WinUtilAPPX -Name $apps[$key].PackageId
$packageList.Add($apps[$key].PackageId)
if ($key -eq "WPFAppxMSTeams") {
# Uninstalls Microsoft Teams Meeting Add-in for Microsoft Office
@@ -41,6 +43,10 @@ function Invoke-WPFAppxRemoval {
}
}
if ($packageList.Count -gt 0) {
Remove-WinUtilProvisionedAPPX -PackageList $packageList.ToArray()
}
Write-Host "================================="
Write-Host "-- AppX Removal Finished ---"
Write-Host "================================="
+2
View File
@@ -65,6 +65,8 @@ function Invoke-WPFButton {
"WPFUpdatessecurity" {Invoke-WPFUpdatessecurity}
"WPFGetInstalled" {Invoke-WPFGetInstalled -CheckBox "winget"}
"WPFGetInstalledTweaks" {Invoke-WPFGetInstalled -CheckBox "tweaks"}
"WPFAppxRemoval" {Invoke-WPFTab "WPFTab6BT"}
"WPFBackToTweaks" {Invoke-WPFTab "WPFTab2BT"}
"WPFRemoveSelectedAppx" {Invoke-WPFAppxRemoval}
"WPFDefaultAppxSelection" {Invoke-WPFPresets "AppxDefault" -checkboxfilterpattern "WPFAppx*"}
"WPFSelectAllAppx" {
+1 -2
View File
@@ -19,13 +19,12 @@ function Invoke-WPFTab {
$tabNumber = [int]($ClickedTab -replace "WPFTab","" -replace "BT","") - 1
$filter = Get-WinUtilVariables -Type ToggleButton | Where-Object {$psitem -like "WPFTab?BT"}
$sync.$tabNav.Items[$tabNumber].IsSelected = $true
($sync.GetEnumerator()).where{$psitem.Key -in $filter} | ForEach-Object {
if ($ClickedTab -ne $PSItem.name) {
$sync[$PSItem.Name].IsChecked = $false
} else {
$sync["$ClickedTab"].IsChecked = $true
$tabNumber = [int]($ClickedTab-replace "WPFTab","" -replace "BT","") - 1
$sync.$tabNav.Items[$tabNumber].IsSelected = $true
}
}
$sync.currentTab = $sync.$tabNav.Items[$tabNumber].Header
+1 -1
View File
@@ -441,7 +441,7 @@ function Invoke-WPFUIElements {
$sync[$entryInfo.Name].Add_Unchecked({
[System.Object]$Sender = $args[0]
Invoke-WPFSelectedCheckboxesUpdate -type "Remove" -checkbox $Sender.name
Invoke-WPFSelectedCheckboxesUpdate -type "Remove" -checkboxName $Sender.name
})
}
}
+113 -21
View File
@@ -5,8 +5,21 @@
BeforeAll {
$script:repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
. (Join-Path $script:repoRoot "functions\private\Remove-WinUtilAPPX.ps1")
. (Join-Path $script:repoRoot "functions\private\Remove-WinUtilProvisionedAPPX.ps1")
. (Join-Path $script:repoRoot "functions\public\Invoke-WPFAppxRemoval.ps1")
$tokens = $null
$parseErrors = $null
$provisionedSourcePath = Join-Path $script:repoRoot "functions\private\Remove-WinUtilProvisionedAPPX.ps1"
$provisionedSourceAst = [System.Management.Automation.Language.Parser]::ParseFile($provisionedSourcePath, [ref]$tokens, [ref]$parseErrors)
$ps5CommandAssignment = $provisionedSourceAst.Find({
param($node)
$node -is [System.Management.Automation.Language.AssignmentStatementAst] -and
$node.Left -is [System.Management.Automation.Language.VariableExpressionAst] -and
$node.Left.VariablePath.UserPath -eq "ps5Command"
}, $true)
$script:provisionedRemovalScriptBlock = $ps5CommandAssignment.Right.Expression.ScriptBlock.GetScriptBlock()
function Write-WinUtilLog {
param($Message, $Level, $Component)
}
@@ -23,18 +36,25 @@ BeforeAll {
param(
[Parameter(ValueFromPipeline = $true)]
$InputObject,
[switch]$AllUsers
$Package,
[switch]$AllUsers,
$ErrorAction
)
process { }
}
function Remove-WinUtilProvisionedAPPX {
param($PackageList)
}
function Get-AppxProvisionedPackage {
param([switch]$Online)
param([switch]$Online, $ErrorAction)
}
function Remove-AppxProvisionedPackage {
param(
[Parameter(ValueFromPipeline = $true)]
$InputObject,
[switch]$Online
[switch]$Online,
$PackageName,
$ErrorAction
)
process { }
}
@@ -58,35 +78,90 @@ Describe "Remove-WinUtilAPPX" {
Mock Write-Host { }
Mock Write-WinUtilLog { }
Mock Get-AppxPackage {
[pscustomobject]@{
Name = $Name
}
@(
[pscustomobject]@{
Name = $Name
PackageFullName = "$Name.FullName"
}
[pscustomobject]@{
Name = $Name
PackageFullName = "$Name.FullName"
}
)
}
Mock Remove-AppxPackage { }
}
It "removes matching installed AppX packages" {
Remove-WinUtilAPPX -Name "Microsoft.Xbox*"
Should -Invoke -CommandName Get-AppxPackage -Times 1 -Exactly -ParameterFilter {
$Name -eq "*Microsoft.Xbox**" -and $AllUsers -eq $true
}
Should -Invoke -CommandName Remove-AppxPackage -Times 1 -Exactly -ParameterFilter {
$Package -eq "*Microsoft.Xbox**.FullName" -and
$AllUsers -eq $true -and
$ErrorAction -eq "Stop"
}
}
It "logs installed AppX removal failures" {
Mock Remove-AppxPackage { throw "Removal failed" }
{ Remove-WinUtilAPPX -Name "Example.Package" } | Should -Not -Throw
Should -Invoke -CommandName Write-WinUtilLog -Times 1 -Exactly -ParameterFilter {
$Level -eq "ERROR" -and
$Component -eq "AppX" -and
$Message -eq "Failed to remove AppX package *Example.Package*.FullName: Removal failed"
}
}
}
Describe "Remove-WinUtilProvisionedAPPX" {
BeforeEach {
Mock Get-AppxProvisionedPackage {
@(
[pscustomobject]@{ DisplayName = "Microsoft.XboxGamingOverlay" }
[pscustomobject]@{ DisplayName = "Microsoft.WindowsCalculator" }
[pscustomobject]@{ DisplayName = "Example.One"; PackageName = "Example.One_1.0" }
[pscustomobject]@{ DisplayName = "Example.Two"; PackageName = "Example.Two_1.0" }
)
}
Mock Remove-AppxProvisionedPackage { }
}
It "removes matching installed and provisioned AppX packages" {
Remove-WinUtilAPPX -Name "Microsoft.Xbox*"
It "queries provisioned packages once for all selected package names" {
& $script:provisionedRemovalScriptBlock "Example.One" "Example.Two"
Should -Invoke -CommandName Get-AppxPackage -Times 1 -Exactly -ParameterFilter {
$Name -eq "Microsoft.Xbox*" -and $AllUsers -eq $true
}
Should -Invoke -CommandName Remove-AppxPackage -Times 1 -Exactly -ParameterFilter {
$InputObject.Name -eq "Microsoft.Xbox*" -and $AllUsers -eq $true
}
Should -Invoke -CommandName Get-AppxProvisionedPackage -Times 1 -Exactly -ParameterFilter {
$Online -eq $true
}
Should -Invoke -CommandName Remove-AppxProvisionedPackage -Times 1 -Exactly -ParameterFilter {
$InputObject.DisplayName -eq "Microsoft.XboxGamingOverlay" -and $Online -eq $true
$PackageName -eq "Example.One_1.0" -and $Online -eq $true
}
Should -Invoke -CommandName Remove-AppxProvisionedPackage -Times 1 -Exactly -ParameterFilter {
$PackageName -eq "Example.Two_1.0" -and $Online -eq $true
}
}
It "surfaces provisioned package removal failures from the child process" {
Mock Remove-AppxProvisionedPackage { throw "DISM failed" }
{ & $script:provisionedRemovalScriptBlock "Example.One" } |
Should -Throw "*Failed to remove provisioned AppX package Example.One_1.0: DISM failed*"
Should -Invoke -CommandName Remove-AppxProvisionedPackage -Times 1 -Exactly -ParameterFilter {
$PackageName -eq "Example.One_1.0" -and
$Online -eq $true -and
$ErrorAction -eq "Stop"
}
}
It "handles child process failures before logging completion" {
$source = Get-Content -Path $provisionedSourcePath -Raw
$source | Should -Match '\$removalOutput = powershell\.exe .* 2>&1'
$source | Should -Match 'if \(\$LASTEXITCODE -ne 0 -or \$null -ne \$removalOutput\)'
$source | Should -Match 'Write-WinUtilLog -Level "ERROR" -Component "AppX" -Message "AppX provisioned package removal failed:'
$source | Should -Match '(?s)AppX provisioned package removal failed:.*return.*AppX provisioned package removal completed\.'
}
}
@@ -190,9 +265,11 @@ Describe "Invoke-WPFAppxRemoval runspace body" {
Mock Get-AppxPackage {
[pscustomobject]@{
Name = $Name
PackageFullName = "$Name.FullName"
}
}
Mock Remove-AppxPackage { }
Mock Remove-WinUtilProvisionedAPPX { }
Mock Get-Package {
[pscustomobject]@{
Name = "Microsoft Teams Meeting Add-in"
@@ -216,10 +293,13 @@ Describe "Invoke-WPFAppxRemoval runspace body" {
& $script:capturedAppxScriptBlock -selected $selected -apps $script:apps
Should -Invoke -CommandName Get-AppxPackage -Times 1 -Exactly -ParameterFilter {
$Name -eq "Example.Package" -and $AllUsers -eq $true
$Name -eq "*Example.Package*" -and $AllUsers -eq $true
}
Should -Invoke -CommandName Remove-AppxPackage -Times 1 -Exactly -ParameterFilter {
$InputObject.Name -eq "Example.Package" -and $AllUsers -eq $true
$Package -eq "*Example.Package*.FullName"
}
Should -Invoke -CommandName Remove-WinUtilProvisionedAPPX -Times 1 -Exactly -ParameterFilter {
$PackageList.Count -eq 1 -and $PackageList[0] -eq "Example.Package"
}
$script:sync.ProcessRunning | Should -BeFalse
}
@@ -239,7 +319,10 @@ Describe "Invoke-WPFAppxRemoval runspace body" {
& $script:capturedAppxScriptBlock -selected $selected -apps $script:apps
Should -Invoke -CommandName Stop-Process -Times 1 -Exactly -ParameterFilter {
$Name -eq "GameBarFTServer"
$Name -eq "GameBarFTServer" -and
$Force -eq $true -and
$Confirm -eq $false -and
$ErrorAction -eq "SilentlyContinue"
}
Should -Invoke -CommandName Set-ItemProperty -Times 1 -Exactly -ParameterFilter {
$Path -eq "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\GameDVR" -and
@@ -247,7 +330,10 @@ Describe "Invoke-WPFAppxRemoval runspace body" {
$Value -eq 0
}
Should -Invoke -CommandName Stop-Process -Times 1 -Exactly -ParameterFilter {
$Name -eq "dllhost"
$Name -eq "dllhost" -and
$Force -eq $true -and
$Confirm -eq $false -and
$ErrorAction -eq "SilentlyContinue"
}
Should -Invoke -CommandName Get-Package -Times 1 -Exactly -ParameterFilter {
$Name -eq "Microsoft Teams*"
@@ -256,6 +342,12 @@ Describe "Invoke-WPFAppxRemoval runspace body" {
$InputObject.Name -eq "Microsoft Teams Meeting Add-in" -and $Force -eq $true
}
Should -Invoke -CommandName Remove-AppxPackage -Times 3 -Exactly
Should -Invoke -CommandName Remove-WinUtilProvisionedAPPX -Times 1 -Exactly -ParameterFilter {
$PackageList.Count -eq 3 -and
$PackageList[0] -eq "Microsoft.XboxGamingOverlay" -and
$PackageList[1] -eq "Microsoft.WindowsNotepad" -and
$PackageList[2] -eq "MSTeams"
}
$script:sync.ProcessRunning | Should -BeFalse
}
}
+8
View File
@@ -19,6 +19,9 @@ BeforeAll {
function Remove-WinUtilAPPX {
param($Name)
}
function Remove-WinUtilProvisionedAPPX {
param($PackageList)
}
function Set-WinUtilDNS {
param($DNSProvider)
}
@@ -89,6 +92,7 @@ Describe "Invoke-WinUtilTweaks" {
Mock Set-WinUtilRegistry { }
Mock Invoke-WinUtilScript { }
Mock Remove-WinUtilAPPX { }
Mock Remove-WinUtilProvisionedAPPX { }
Mock Write-WinUtilLog { }
Mock Write-Warning { }
}
@@ -118,6 +122,9 @@ Describe "Invoke-WinUtilTweaks" {
Should -Invoke -CommandName Remove-WinUtilAPPX -Times 1 -Exactly -ParameterFilter {
$Name -eq "Microsoft.ExampleApp"
}
Should -Invoke -CommandName Remove-WinUtilProvisionedAPPX -Times 1 -Exactly -ParameterFilter {
$PackageList.Count -eq 1 -and $PackageList[0] -eq "Microsoft.ExampleApp"
}
}
It "uses original registry values and service startup types in undo mode" {
@@ -137,6 +144,7 @@ Describe "Invoke-WinUtilTweaks" {
$Name -eq "WPFTweaksExample" -and $ScriptBlock.ToString() -eq "Write-Output 'undo tweak'"
}
Should -Invoke -CommandName Remove-WinUtilAPPX -Times 0 -Exactly
Should -Invoke -CommandName Remove-WinUtilProvisionedAPPX -Times 0 -Exactly
}
It "keeps a user-changed service startup type by default" {
+23 -1
View File
@@ -134,7 +134,6 @@ Describe "XAML document" {
"WPFTab3BT",
"WPFTab4BT",
"WPFTab5BT",
"WPFTab6BT",
"SearchBar",
"SearchBarClearButton",
"appscategory",
@@ -148,6 +147,7 @@ Describe "XAML document" {
"WPFAdvanced",
"WPFClearTweaksSelection",
"WPFGetInstalledTweaks",
"WPFAppxRemoval",
"WPFTweaksbutton",
"WPFUndoall",
"WPFUpdatesdefault",
@@ -157,6 +157,7 @@ Describe "XAML document" {
"WPFGetInstalledAppx",
"WPFSelectAllAppx",
"WPFClearAppxSelection",
"WPFBackToTweaks",
"WPFRemoveSelectedAppx"
)
@@ -223,6 +224,27 @@ Describe "XAML document" {
}
}
}
It "opens AppX removal from Tweaks and provides a return path" {
$navPanel = $script:xaml.SelectSingleNode('//*[local-name()="StackPanel"][@Name="NavDockPanel"]')
$tweaksTab = $script:xaml.SelectSingleNode('//*[local-name()="TabItem"][@Name="WPFTab2"]')
$appxTab = $script:xaml.SelectSingleNode('//*[local-name()="TabItem"][@Name="WPFTab6"]')
$openButton = $tweaksTab.SelectSingleNode('.//*[local-name()="Button"][@Name="WPFAppxRemoval"]')
$buttonNames = @($openButton.ParentNode.SelectNodes('./*[local-name()="Button"]') | ForEach-Object { $_.GetAttribute("Name") })
$getInstalledIndex = [array]::IndexOf($buttonNames, "WPFGetInstalledTweaks")
$openAppxIndex = [array]::IndexOf($buttonNames, "WPFAppxRemoval")
$buttonSource = Get-Content -Path $script:buttonScriptPath -Raw
$tabSource = Get-Content -Path (Join-Path $script:functionRoot "public\Invoke-WPFTab.ps1") -Raw
$navPanel.SelectSingleNode('./*[local-name()="ToggleButton"][@Name="WPFTab6BT"]') | Should -BeNullOrEmpty
$openButton.GetAttribute("Content").Trim() | Should -Be "AppX Removal"
$openAppxIndex | Should -Be ($getInstalledIndex + 1)
$appxTab.SelectSingleNode('.//*[local-name()="Button"][@Name="WPFBackToTweaks"]') | Should -Not -BeNullOrEmpty
$appxTab.SelectSingleNode('.//*[local-name()="Button"][@Name="WPFRemoveSelectedAppx"]') | Should -Not -BeNullOrEmpty
$buttonSource | Should -Match '"WPFAppxRemoval"\s*\{Invoke-WPFTab "WPFTab6BT"\}'
$buttonSource | Should -Match '"WPFBackToTweaks"\s*\{Invoke-WPFTab "WPFTab2BT"\}'
$tabSource | Should -Match '\$sync\.\$tabNav\.Items\[\$tabNumber\]\.IsSelected = \$true'
}
}
Describe "XAML and sync wiring" {
+2 -8
View File
@@ -983,14 +983,6 @@
</TextBlock>
</ToggleButton.Content>
</ToggleButton>
<ToggleButton Margin="0,0,5,0" Height="{DynamicResource TabButtonHeight}" Width="Auto" MinWidth="{DynamicResource TabButtonWidth}"
Background="{DynamicResource ButtonAppxBackgroundColor}" Foreground="{DynamicResource ButtonAppxForegroundColor}" FontWeight="Bold" Name="WPFTab6BT">
<ToggleButton.Content>
<TextBlock FontSize="{DynamicResource TabButtonFontSize}" Background="Transparent" Foreground="{DynamicResource ButtonAppxForegroundColor}">
<Underline>A</Underline>ppX Removal
</TextBlock>
</ToggleButton.Content>
</ToggleButton>
</StackPanel>
<!-- Search Bar and Action Buttons -->
@@ -1237,6 +1229,7 @@
<Button Name="WPFAdvanced" Content=" Advanced " Margin="2" Width="{DynamicResource ButtonWidth}" Height="{DynamicResource ButtonHeight}"/>
<Button Name="WPFClearTweaksSelection" Content=" Clear " Margin="2" Width="{DynamicResource ButtonWidth}" Height="{DynamicResource ButtonHeight}"/>
<Button Name="WPFGetInstalledTweaks" Content=" Get Installed Tweaks " Margin="2" Width="{DynamicResource ButtonWidth}" Height="{DynamicResource ButtonHeight}"/>
<Button Name="WPFAppxRemoval" Content=" AppX Removal " Margin="2" Width="{DynamicResource ButtonWidth}" Height="{DynamicResource ButtonHeight}"/>
</StackPanel>
</StackPanel>
@@ -1717,6 +1710,7 @@
<Border Grid.Row="1" Background="{DynamicResource MainBackgroundColor}" BorderBrush="{DynamicResource BorderColor}" BorderThickness="1" CornerRadius="5" HorizontalAlignment="Stretch" Padding="10">
<WrapPanel Orientation="Horizontal" HorizontalAlignment="Left" VerticalAlignment="Center">
<Button Name="WPFBackToTweaks" Content="Back to Tweaks" Margin="5" Width="{DynamicResource ButtonWidth}" Height="{DynamicResource ButtonHeight}"/>
<Button Name="WPFRemoveSelectedAppx" Content="Remove Selected" Margin="5" Width="{DynamicResource ButtonWidth}" Height="{DynamicResource ButtonHeight}"/>
</WrapPanel>
</Border>