Refactor: Cleanup app removal, remove legacy app list generator and CustomAppsList file support (#662)

* remove support for uninstalling old sunset apps

* Add color legend on app removal screen

* Remove legacy app list generator and custom apps file support
Replaced by GUI config export/import, dynamic RemovalMethod, and
CLI app removal settings saved to LastUsedSettings.json.

* Verify app removal by checking actual installation state instead of trusting winget output
This commit is contained in:
Jeffrey
2026-06-22 22:13:01 +02:00
committed by GitHub
parent 71e3f2e44d
commit d1fe541b62
22 changed files with 865 additions and 477 deletions

4
.gitignore vendored
View File

@@ -1,7 +1,3 @@
LastSettings
SavedSettings
LastUsedSettings.json LastUsedSettings.json
CustomAppsList
Logs/* Logs/*
Win11Debloat.log
Backups/* Backups/*

File diff suppressed because it is too large Load Diff

View File

@@ -339,18 +339,6 @@
"MinVersion": null, "MinVersion": null,
"MaxVersion": null "MaxVersion": null
}, },
{
"FeatureId": "RemoveAppsCustom",
"Label": "Remove custom selection of apps",
"Category": null,
"RegistryKey": null,
"ApplyText": "Removing selected apps",
"UndoLabel": null,
"ApplyUndoText": null,
"RegistryUndoKey": null,
"MinVersion": null,
"MaxVersion": null
},
{ {
"FeatureId": "RemoveGamingApps", "FeatureId": "RemoveGamingApps",
"Label": "Remove the Xbox App and Xbox Gamebar", "Label": "Remove the Xbox App and Xbox Gamebar",

View File

@@ -184,6 +184,11 @@
<SolidColorBrush x:Key="ValidationErrorColor" Color="#c42b1c"/> <SolidColorBrush x:Key="ValidationErrorColor" Color="#c42b1c"/>
<SolidColorBrush x:Key="ValidationSuccessColor" Color="#28a745"/> <SolidColorBrush x:Key="ValidationSuccessColor" Color="#28a745"/>
<!-- App recommendation dot / legend colors -->
<SolidColorBrush x:Key="AppRecommendationSafeColor" Color="#4CAF50"/>
<SolidColorBrush x:Key="AppRecommendationOptionalColor" Color="#FFC107"/>
<SolidColorBrush x:Key="AppRecommendationUnsafeColor" Color="#F44336"/>
<!-- Title Bar Button Style --> <!-- Title Bar Button Style -->
<Style x:Key="TitleBarButton" TargetType="Button"> <Style x:Key="TitleBarButton" TargetType="Button">
<Setter Property="Background" Value="Transparent"/> <Setter Property="Background" Value="Transparent"/>
@@ -703,10 +708,32 @@
</Border> </Border>
</Grid> </Grid>
<!-- Status Info --> <!-- Status Info & Color Legend -->
<StackPanel Grid.Row="1" Orientation="Horizontal" Margin="20,0,20,0"> <Grid Grid.Row="1" Margin="20,0">
<TextBlock x:Name="AppSelectionStatus" Text="" Foreground="{DynamicResource AppFgColor}" Margin="10,0,0,5" HorizontalAlignment="Left"/> <Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock x:Name="AppSelectionStatus" Grid.Column="0" Text="" Foreground="{DynamicResource AppFgColor}" Margin="10,0,0,5" HorizontalAlignment="Left" VerticalAlignment="Bottom"/>
<StackPanel x:Name="AppColorLegend" Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Bottom" Margin="0,0,10,5">
<TextBlock Text="Legend:" Foreground="{DynamicResource AppFgColor}" FontSize="12" FontWeight="SemiBold" VerticalAlignment="Center" Margin="0,0,10,0" Opacity="0.8"/>
<!-- Recommended (safe) -->
<StackPanel Orientation="Horizontal" Margin="0,0,16,0" ToolTip="[Recommended] Safe to remove for most users">
<Ellipse Width="9" Height="9" Fill="{DynamicResource AppRecommendationSafeColor}" VerticalAlignment="Center" Margin="0,0,6,0"/>
<TextBlock Text="Recommended" Foreground="{DynamicResource AppFgColor}" FontSize="12" VerticalAlignment="Center" Opacity="0.8"/>
</StackPanel> </StackPanel>
<!-- Optional (default) -->
<StackPanel Orientation="Horizontal" Margin="0,0,16,0" ToolTip="[Optional] Can be safely removed if you don't need this app">
<Ellipse Width="9" Height="9" Fill="{DynamicResource AppRecommendationOptionalColor}" VerticalAlignment="Center" Margin="0,0,6,0"/>
<TextBlock Text="Optional" Foreground="{DynamicResource AppFgColor}" FontSize="12" VerticalAlignment="Center" Opacity="0.8"/>
</StackPanel>
<!-- Not Recommended (unsafe) -->
<StackPanel Orientation="Horizontal" ToolTip="[Not Recommended] Only remove if you know what you are doing">
<Ellipse Width="9" Height="9" Fill="{DynamicResource AppRecommendationUnsafeColor}" VerticalAlignment="Center" Margin="0,0,6,0"/>
<TextBlock Text="Not Recommended" Foreground="{DynamicResource AppFgColor}" FontSize="12" VerticalAlignment="Center" Opacity="0.8"/>
</StackPanel>
</StackPanel>
</Grid>
</Grid> </Grid>
</DockPanel> </DockPanel>
</TabItem> </TabItem>

View File

@@ -1,6 +1,22 @@
<#
.SYNOPSIS
Returns a list of installed apps from winget as structured objects.
# Run winget list and return installed apps. .DESCRIPTION
# Use -NonBlocking to keep the UI responsive (GUI mode) via Invoke-NonBlocking. Runs `winget list` and parses the output into PSCustomObject arrays.
Use -NonBlocking to keep the UI responsive in GUI mode; otherwise
runs synchronously with an optional timeout.
.PARAMETER TimeOut
Maximum seconds to wait for winget to complete. Default is 10.
.PARAMETER NonBlocking
When set, runs via Invoke-NonBlocking so the GUI thread stays responsive.
.OUTPUTS
PSCustomObject[] with Name and Id properties. Returns $null on
failure, or an empty array when winget succeeds but lists no apps.
#>
function GetInstalledAppsViaWinget { function GetInstalledAppsViaWinget {
param ( param (
[int]$TimeOut = 10, [int]$TimeOut = 10,
@@ -11,13 +27,76 @@ function GetInstalledAppsViaWinget {
$fetchBlock = { $fetchBlock = {
param($timeOut) param($timeOut)
$job = Start-Job { return winget list --accept-source-agreements --disable-interactivity } $job = Start-Job {
$rawOutput = $null
try {
$originalEncoding = [Console]::OutputEncoding
[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new()
try {
$rawOutput = winget list --accept-source-agreements --disable-interactivity
}
finally {
[Console]::OutputEncoding = $originalEncoding
}
return $rawOutput
}
catch {
return $null
}
}
$done = $job | Wait-Job -Timeout $timeOut $done = $job | Wait-Job -Timeout $timeOut
if ($done) { if ($done) {
$result = Receive-Job -Job $job $result = Receive-Job -Job $job
Remove-Job -Job $job -ErrorAction SilentlyContinue Remove-Job -Job $job -ErrorAction SilentlyContinue
return $result
if (-not $result) { return $null }
# winget list outputs:
# [progress line] / [blank] / header / --- separator / data rows
$textOutput = $result -join "`n"
$lines = $textOutput -split "`r`n|`n"
# Find the separator line to know where data starts
$dataStart = -1
for ($i = 0; $i -lt $lines.Count; $i++) {
if ($lines[$i] -match '^-{3,}') {
$dataStart = $i + 1
break
} }
}
if ($dataStart -lt 0 -or $dataStart -ge $lines.Count) { return @() }
$apps = [System.Collections.Generic.List[object]]::new()
for ($i = $dataStart; $i -lt $lines.Count; $i++) {
$line = $lines[$i]
if ($line.Trim() -eq '') { continue }
try {
# Split on 2+ spaces; extract Name and Id columns.
$fields = [regex]::Split($line.Trim(), '\s{2,}')
if ($fields.Count -lt 2) { continue }
$name = $fields[0].Trim()
$id = $fields[1].Trim()
if (-not $id) { continue }
$null = $apps.Add([PSCustomObject]@{
Name = $name
Id = $id
})
}
catch {
# Skip lines that can't be parsed
}
}
return @($apps)
}
Remove-Job -Job $job -Force -ErrorAction SilentlyContinue Remove-Job -Job $job -Force -ErrorAction SilentlyContinue
return $null return $null
} }

View File

@@ -4,11 +4,12 @@
.DESCRIPTION .DESCRIPTION
Iterates over the provided list of app identifiers and removes each one. Iterates over the provided list of app identifiers and removes each one.
Apps are removed via WinGet (for OneDrive and Microsoft Edge) or via The removal method (winget vs. Appx cmdlets) is determined per-app from
Remove-AppxPackage / Remove-ProvisionedAppxPackage (for all other apps). Apps.json. Microsoft Edge is deferred to the end of the loop so that all
The target scope is determined by script-level parameters: winget attempts run before any force-remove prompt. A scheduled task is
-Sysprep removes from the OS image for future users; -User targets a only created when the User or Sysprep parameter was passed.
specific user; otherwise the current user is targeted. After each winget removal, the system is checked to confirm whether the
app is still installed before reporting an error.
.PARAMETER appsList .PARAMETER appsList
An array of app package identifiers to remove (e.g. 'Microsoft.BingNews'). An array of app package identifiers to remove (e.g. 'Microsoft.BingNews').
@@ -19,7 +20,6 @@
.EXAMPLE .EXAMPLE
RemoveApps -appsList (GenerateAppsList) RemoveApps -appsList (GenerateAppsList)
#> #>
# Removes apps specified during function call based on the target scope.
function RemoveApps { function RemoveApps {
param ( param (
$appslist $appslist
@@ -34,109 +34,167 @@ function RemoveApps {
return return
} }
# Determine target from script-level params, defaulting to AllUsers
$targetUser = GetTargetUserForAppRemoval $targetUser = GetTargetUserForAppRemoval
$appIndex = 0
$appCount = @($appsList).Count $appCount = @($appsList).Count
$appIndex = 0
$edgeIds = @('Microsoft.Edge', 'XPFFTQ037JWMHS') $edgeIds = @('Microsoft.Edge', 'XPFFTQ037JWMHS')
$edgeUninstallSucceeded = $false $edgeAppsInList = @()
$edgeScheduledTaskAdded = $false $wingetRemovedApps = @()
Foreach ($app in $appsList) { Foreach ($app in $appsList) {
if ($script:CancelRequested) { if ($script:CancelRequested) { return }
return
}
$appIndex++ $appIndex++
# Update step name and sub-progress to show which app is being removed (only for bulk removal)
if ($script:ApplySubStepCallback -and $appCount -gt 1) { if ($script:ApplySubStepCallback -and $appCount -gt 1) {
& $script:ApplySubStepCallback "Removing apps... ($appIndex/$appCount)" $appIndex $appCount & $script:ApplySubStepCallback "Removing apps ($appIndex/$appCount)" $appIndex $appCount
}
# Microsoft Edge is handled after the loop to avoid duplicate scheduled tasks and allow fallback if winget fails
if ($edgeIds -contains $app) {
$edgeAppsInList += $app
continue
} }
Write-Host "Removing $app" Write-Host "Removing $app"
# Use WinGet only to remove OneDrive and Edge if ((Get-AppRemovalMethod $app) -eq 'WinGet') {
if (($app -eq "Microsoft.OneDrive") -or ($edgeIds -contains $app)) { Remove-WinGetApp -app $app
if ($script:WingetInstalled -eq $false) { $wingetRemovedApps += $app
Write-Host "WinGet is either not installed or is outdated, $app could not be removed" -ForegroundColor Red
continue
}
$isEdgeId = $edgeIds -contains $app
$appName = if ($isEdgeId) { 'Microsoft_Edge' } else { $app -replace '\.', '_' }
# Uninstall app via WinGet, or create a scheduled task to uninstall it later
if ($script:Params.ContainsKey("User")) {
if (-not ($isEdgeId -and $edgeScheduledTaskAdded)) {
ImportRegistryFile "Adding scheduled task to uninstall $app for user $(GetUserName)..." "Uninstall_$($appName).reg"
if ($isEdgeId) { $edgeScheduledTaskAdded = $true }
}
}
elseif ($script:Params.ContainsKey("Sysprep")) {
if (-not ($isEdgeId -and $edgeScheduledTaskAdded)) {
ImportRegistryFile "Adding scheduled task to uninstall $app after for new users..." "Uninstall_$($appName).reg"
if ($isEdgeId) { $edgeScheduledTaskAdded = $true }
}
} }
else { else {
# Uninstall app via WinGet Remove-AppxApp -app $app -targetUser $targetUser
$wingetResult = Invoke-NonBlocking -ScriptBlock { }
param($appId)
$global:LASTEXITCODE = 0
$output = winget uninstall --accept-source-agreements --disable-interactivity --id $appId
[PSCustomObject]@{ ExitCode = $LASTEXITCODE; Output = $output }
} -ArgumentList $app
# winget reports success/failure via its exit code, which is locale-independent.
# The previous match on English console text silently passed on non-English Windows.
# Treat a null result (timed out / not run) or any non-zero exit code as a failure.
$wingetFailed = ($null -eq $wingetResult) -or ($wingetResult.ExitCode -ne 0)
if ($isEdgeId) {
if (-not $wingetFailed) {
$edgeUninstallSucceeded = $true
} }
# Prompt immediately after the final selected Edge ID attempt (if all attempts failed) # Remove Microsoft Edge
$hasRemainingEdgeIds = $false if ($edgeAppsInList.Count -gt 0) {
if ($appIndex -lt $appCount) { Remove-EdgeApp -edgeAppsInList $edgeAppsInList
$remainingApps = @($appsList)[($appIndex)..($appCount - 1)]
$hasRemainingEdgeIds = @($remainingApps | Where-Object { $edgeIds -contains $_ }).Count -gt 0
} }
if (-not $hasRemainingEdgeIds -and -not $edgeUninstallSucceeded) { # Check whether any winget-removed apps are still present, and report errors for each one.
Write-Host "Unable to uninstall Microsoft Edge via WinGet" -ForegroundColor Red if ($wingetRemovedApps.Count -gt 0 -or $edgeAppsInList.Count -gt 0) {
$postRemovalList = if ($script:WingetInstalled) { GetInstalledAppsViaWinget -TimeOut 10 -NonBlocking } else { $null }
if ($script:GuiWindow) { foreach ($app in $wingetRemovedApps) {
$result = Show-MessageBox -Message 'Unable to uninstall Microsoft Edge via WinGet. Would you like to forcefully uninstall it? NOT RECOMMENDED!' -Title 'Force Uninstall Microsoft Edge?' -Button 'YesNo' -Icon 'Warning' if (Test-AppStillInstalled -appId $app -InstalledList $postRemovalList) {
if ($result -eq 'Yes') {
Write-Host ""
ForceRemoveEdge
}
}
elseif ($( Read-Host -Prompt "Would you like to forcefully uninstall Microsoft Edge? NOT RECOMMENDED! (y/n)" ) -eq 'y') {
Write-Host ""
ForceRemoveEdge
}
}
}
elseif ($wingetFailed) {
Write-Host "Unable to uninstall $app via WinGet" -ForegroundColor Red Write-Host "Unable to uninstall $app via WinGet" -ForegroundColor Red
} }
} }
continue # Verify Edge separately (triggers its own force-remove path if still installed)
$edgeStillInstalled = $false
foreach ($edgeApp in $edgeAppsInList) {
if (Test-AppStillInstalled -appId $edgeApp -InstalledList $postRemovalList) {
$edgeStillInstalled = $true
break
}
}
if ($edgeStillInstalled) {
Write-Host "Unable to uninstall Microsoft Edge via WinGet" -ForegroundColor Red
Request-EdgeForceRemove
}
} }
# Use Remove-AppxPackage to remove all other apps Write-Host ""
}
<#
.SYNOPSIS
Uninstalls a non-Edge app via WinGet and/or schedules its removal.
.DESCRIPTION
Runs winget uninstall for a single app. If the User or Sysprep
parameter was passed, also schedules removal for future logins.
After uninstall, the system is checked to confirm whether the app
is still present — winget output is not trusted on its
own, as it sometimes reports failure after a successful removal.
Edge apps are handled separately after the main loop.
.PARAMETER app
The WinGet package ID to uninstall (e.g. 'Microsoft.BingNews').
#>
function Remove-WinGetApp {
param([string]$app)
if (-not $script:WingetInstalled) {
Write-Host "ERROR: WinGet is either not installed or is outdated, $app could not be removed" -ForegroundColor Red
return
}
if ($script:Params.ContainsKey("User")) {
Write-Host "Adding scheduled task to uninstall $app for user $(GetUserName)..."
Set-RunOnceWingetTask -appId $app
}
elseif ($script:Params.ContainsKey("Sysprep")) {
Write-Host "Adding scheduled task to uninstall $app for new users..."
Set-RunOnceWingetTask -appId $app
}
Invoke-NonBlocking -ScriptBlock {
param($appId)
winget uninstall --accept-source-agreements --disable-interactivity --id $appId
} -ArgumentList $app
}
<#
.SYNOPSIS
Removes Microsoft Edge via WinGet (both AppIds), with fallback to force-remove.
.DESCRIPTION
Edge has multiple package IDs. Runs winget uninstall for each one,
then creates a single scheduled task if the User or Sysprep parameter
was passed. After all attempts, the system is checked to confirm
whether Edge is still present. The force-remove prompt only
appears if Edge remains installed — winget false positives are ignored.
.PARAMETER edgeAppsInList
The Edge AppIds that appear in the removal list (one or both).
#>
function Remove-EdgeApp {
param([string[]]$edgeAppsInList)
if (-not $script:WingetInstalled) {
Write-Host "ERROR: WinGet is either not installed or is outdated, Microsoft Edge could not be removed" -ForegroundColor Red
return
}
if ($script:Params.ContainsKey("User")) {
Write-Host "Adding scheduled task to uninstall Microsoft Edge for user $(GetUserName)..."
Set-RunOnceWingetTask -appId 'Microsoft.Edge'
}
elseif ($script:Params.ContainsKey("Sysprep")) {
Write-Host "Adding scheduled task to uninstall Microsoft Edge for new users..."
Set-RunOnceWingetTask -appId 'Microsoft.Edge'
}
foreach ($edgeApp in $edgeAppsInList) {
Write-Host "Removing $edgeApp"
Invoke-NonBlocking -ScriptBlock {
param($appId)
winget uninstall --accept-source-agreements --disable-interactivity --id $appId
} -ArgumentList $edgeApp
}
}
<#
.SYNOPSIS
Removes an app via Remove-AppxPackage / Remove-ProvisionedAppxPackage.
.PARAMETER app
The package identifier to remove (e.g. 'Clipchamp.Clipchamp').
.PARAMETER targetUser
Target scope: "AllUsers", "CurrentUser", or a specific username.
#>
function Remove-AppxApp {
param([string]$app, [string]$targetUser)
$appPattern = '*' + $app + '*' $appPattern = '*' + $app + '*'
try { try {
switch ($targetUser) { switch ($targetUser) {
"AllUsers" { "AllUsers" {
# Remove installed app for all existing users, and from OS image
Invoke-NonBlocking -ScriptBlock { Invoke-NonBlocking -ScriptBlock {
param($pattern) param($pattern)
Get-AppxPackage -Name $pattern -AllUsers | Remove-AppxPackage -AllUsers -ErrorAction Continue Get-AppxPackage -Name $pattern -AllUsers | Remove-AppxPackage -AllUsers -ErrorAction Continue
@@ -144,14 +202,12 @@ function RemoveApps {
} -ArgumentList $appPattern } -ArgumentList $appPattern
} }
"CurrentUser" { "CurrentUser" {
# Remove installed app for current user only
Invoke-NonBlocking -ScriptBlock { Invoke-NonBlocking -ScriptBlock {
param($pattern) param($pattern)
Get-AppxPackage -Name $pattern | Remove-AppxPackage -ErrorAction Continue Get-AppxPackage -Name $pattern | Remove-AppxPackage -ErrorAction Continue
} -ArgumentList $appPattern } -ArgumentList $appPattern
} }
default { default {
# Target is a specific username - remove app for that user only
Invoke-NonBlocking -ScriptBlock { Invoke-NonBlocking -ScriptBlock {
param($pattern, $user) param($pattern, $user)
$userAccount = New-Object System.Security.Principal.NTAccount($user) $userAccount = New-Object System.Security.Principal.NTAccount($user)
@@ -166,5 +222,171 @@ function RemoveApps {
} }
} }
Write-Host "" <#
.SYNOPSIS
Checks whether an app package is still installed after a removal attempt.
.DESCRIPTION
Checks Get-AppxPackage across all users first (fast, no process launch),
then falls back to a pre-fetched or live winget list for non-Appx packages.
Uses Test-AppInWingetList which provides exact-match-first with substring
fallback against the parsed winget objects.
Returns $true if the app is still present, $false otherwise.
.PARAMETER appId
The package identifier to check (e.g. 'Microsoft.BingNews').
.PARAMETER InstalledList
Optional pre-fetched array of winget objects from GetInstalledAppsViaWinget.
When provided, used directly; otherwise a live winget call is made.
#>
function Test-AppStillInstalled {
param(
[string]$appId,
[object[]]$InstalledList
)
# Check Get-AppxPackage for all users first (fast, covers all Store apps).
if (Get-AppxPackage -Name "$appId" -AllUsers -ErrorAction SilentlyContinue) {
return $true
}
# Use the pre-fetched list if provided; otherwise fall back to a live winget call.
if ($InstalledList) {
return (Test-AppInWingetList -appId $appId -InstalledList $InstalledList)
}
if ($script:WingetInstalled) {
$liveList = GetInstalledAppsViaWinget -TimeOut 10 -NonBlocking
if (Test-AppInWingetList -appId $appId -InstalledList $liveList) {
return $true
}
}
else {
Write-Warning "Unable to verify whether '$appId' is still installed (WinGet is unavailable)"
}
return $false
}
<#
.SYNOPSIS
Returns the removal method for an app identifier.
.DESCRIPTION
Parses Apps.json once (cached in script scope) to build a lookup of
AppId -> RemovalMethod. Returns 'WinGet' if the app should be removed
via winget, or 'Appx' if via Remove-AppxPackage. Defaults to 'Appx'
for unknown IDs.
.PARAMETER appId
The package identifier (e.g. 'Clipchamp.Clipchamp').
#>
function Get-AppRemovalMethod {
param([string]$appId)
if (-not $script:AppRemovalMethodCache) {
$script:AppRemovalMethodCache = @{}
try {
if (Test-Path $script:AppsListFilePath) {
$appsJson = Get-Content -Path $script:AppsListFilePath -Raw | ConvertFrom-Json
foreach ($appData in $appsJson.Apps) {
$rawMethod = $appData.RemovalMethod
$method = if ($rawMethod -and $rawMethod -eq 'WinGet') { 'WinGet' } else { 'Appx' }
if ($appData.AppId -is [array]) {
foreach ($id in $appData.AppId) { $script:AppRemovalMethodCache[$id.Trim()] = $method }
}
else {
$script:AppRemovalMethodCache[$appData.AppId.Trim()] = $method
}
}
}
}
catch {
Write-Warning "Failed to load app removal methods from '$script:AppsListFilePath'. Defaulting unknown apps to Appx. Error: $_"
}
}
if ($script:AppRemovalMethodCache.ContainsKey($appId)) {
return $script:AppRemovalMethodCache[$appId]
}
return 'Appx'
}
<#
.SYNOPSIS
Prompts the user to forcefully remove Microsoft Edge when winget cannot uninstall it.
.DESCRIPTION
Only invoked after it has been confirmed that Edge is still present
following all winget uninstall attempts. In GUI mode, displays a
warning message box; in CLI mode, prompts via Read-Host. On
confirmation, performs a force-remove of the Edge package.
#>
function Request-EdgeForceRemove {
if ($script:GuiWindow) {
$result = Show-MessageBox -Message 'Unable to uninstall Microsoft Edge via WinGet. Would you like to forcefully uninstall it? NOT RECOMMENDED!' -Title 'Force Uninstall Microsoft Edge?' -Button 'YesNo' -Icon 'Warning'
if ($result -eq 'Yes') {
Write-Host ""
ForceRemoveEdge
}
}
elseif ($(Read-Host -Prompt "Would you like to forcefully uninstall Microsoft Edge? NOT RECOMMENDED! (y/n)") -eq 'y') {
Write-Host ""
ForceRemoveEdge
}
}
<#
.SYNOPSIS
Dynamically sets a RunOnce registry key to schedule a winget uninstall.
.DESCRIPTION
Writes directly to HKEY_USERS\Default\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce
via the PowerShell registry API within Invoke-WithTargetUserHive,
which handles hive loading and HKEY_USERS\Default → SID remapping.
Used instead of static .reg files to avoid file dependency for each WinGet app.
The winget command is Base64-encoded and invoked via powershell.exe -EncodedCommand
rather than interpolated directly into cmd.exe /c. This prevents shell metacharacters
(such as &, |, <, >, ^, ") in the app ID from being interpreted as command syntax,
even if future catalog updates introduce IDs containing those characters.
.PARAMETER appId
The winget package ID to schedule for uninstall (e.g. 'XP9CXNGPPJ97XX').
#>
function Set-RunOnceWingetTask {
param([string]$appId)
$targetUserName = if ($script:Params.ContainsKey("Sysprep")) { "Default" } else { $script:Params.Item("User") }
# Sanitize appId for use in registry value names (backslashes are path separators)
$safeAppId = $appId.Replace('\', '_')
$taskName = "Uninstall_$safeAppId"
# Escape single quotes in appId, then wrap in single quotes so cmd/pwsh metacharacters
# like & | < > ^ " are treated as literals. Base64-encode the whole command so the
# RunOnce value contains only [A-Za-z0-9+/=] — safe in any shell parser.
$escapedAppId = $appId.Replace("'", "''")
$wingetCommand = "winget uninstall --accept-source-agreements --disable-interactivity --id '$escapedAppId'"
$encodedWingetCommand = [Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($wingetCommand))
$operation = [PSCustomObject]@{
KeyPath = 'HKEY_USERS\Default\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce'
ValueName = $taskName
ValueType = 'String'
ValueData = "powershell.exe -NoProfile -EncodedCommand $encodedWingetCommand"
OperationType = 'SetValue'
}
try {
Invoke-WithTargetUserHive -TargetUserName $targetUserName -ScriptBlock {
param($op)
Invoke-RegistryOperation -Operation $op -RegFilePath '<dynamic>'
} -ArgumentObject $operation
}
catch {
Write-Host "Failed to schedule uninstall task for $($appId): $_" -ForegroundColor Red
}
} }

View File

@@ -0,0 +1,43 @@
<#
.SYNOPSIS
Checks whether an app ID appears in a parsed winget installed list.
.DESCRIPTION
Tries an exact match against the .Id property first. When that
fails, falls back to a substring search guarded by a word-boundary
regex so that short IDs don't accidentally match longer ones
(e.g. 'Microsoft.Edge' will not match 'Microsoft.EdgeDev').
.PARAMETER appId
The identifier to search for (e.g. 'Microsoft.Copilot').
.PARAMETER InstalledList
An array of PSCustomObject from GetInstalledAppsViaWinget.
#>
function Test-AppInWingetList {
param(
[string]$appId,
[object[]]$InstalledList
)
if (-not $InstalledList) { return $false }
# Normalize to array
$list = @($InstalledList)
# Exact match first (fast and precise)
if ($list.Id -contains $appId) {
return $true
}
# Substring fallback with word-boundary guard
$boundaryPattern = '(?<![a-zA-Z0-9])' + [regex]::Escape($appId) + '(?![a-zA-Z0-9])'
foreach ($entry in $list) {
if ($entry.Id -like "*$appId*" -and $entry.Id -match $boundaryPattern) {
return $true
}
}
return $false
}

View File

@@ -1,4 +1,17 @@
# Prints all pending changes that will be made by the script <#
.SYNOPSIS
Prints a summary of all pending changes to the console for the user to review.
.DESCRIPTION
Iterates over every non-control parameter in $script:Params and emits a
human-readable line for each change that will be applied. For the
'RemoveApps' parameter the list of targeted app names is displayed
inline. Feature labels are resolved from Features.json when available;
otherwise the raw parameter name is used as a fallback.
After printing the summary the function pauses until the user presses
Enter, giving them an opportunity to review and cancel via Ctrl+C.
#>
function PrintPendingChanges { function PrintPendingChanges {
Write-Output "Win11Debloat will make the following changes:" Write-Output "Win11Debloat will make the following changes:"
@@ -31,19 +44,6 @@ function PrintPendingChanges {
Write-Host $appsList -ForegroundColor DarkGray Write-Host $appsList -ForegroundColor DarkGray
continue continue
} }
'RemoveAppsCustom' {
$appsList = LoadAppsFromFile $script:CustomAppsListFilePath
if ($appsList.Count -eq 0) {
Write-Host "No valid apps were selected for removal" -ForegroundColor Yellow
Write-Output ""
continue
}
Write-Output "- Remove $($appsList.Count) apps:"
Write-Host $appsList -ForegroundColor DarkGray
continue
}
default { default {
if ($script:Features -and $script:Features.ContainsKey($parameterName)) { if ($script:Features -and $script:Features.ContainsKey($parameterName)) {
$message = $script:Features[$parameterName].Label $message = $script:Features[$parameterName].Label

View File

@@ -8,7 +8,8 @@ function ShowCLIAppRemoval {
if ($result -eq $true) { if ($result -eq $true) {
Write-Output "You have selected $($script:SelectedApps.Count) apps for removal" Write-Output "You have selected $($script:SelectedApps.Count) apps for removal"
AddParameter 'RemoveAppsCustom' AddParameter 'RemoveApps'
AddParameter 'Apps' ($script:SelectedApps -join ',')
SaveSettings SaveSettings

View File

@@ -25,7 +25,8 @@ function ShowCLIDefaultModeOptions {
AddParameter 'Apps' 'Default' AddParameter 'Apps' 'Default'
} }
'2' { '2' {
AddParameter 'RemoveAppsCustom' AddParameter 'RemoveApps'
AddParameter 'Apps' ($script:SelectedApps -join ',')
if ($DisableGameBarIntegrationInput) { if ($DisableGameBarIntegrationInput) {
AddParameter 'DisableDVR' AddParameter 'DisableDVR'

View File

@@ -21,9 +21,11 @@ function Invoke-FeatureApply {
$feature = $script:Features[$FeatureId] $feature = $script:Features[$FeatureId]
} }
$applyText = if ($feature -and $feature.ApplyText) { $feature.ApplyText } else { $FeatureId }
# ---- Registry-backed features: import .reg file, then handle side effects ---- # ---- Registry-backed features: import .reg file, then handle side effects ----
if ($feature -and $feature.RegistryKey -and $feature.ApplyText) { if ($feature -and $feature.RegistryKey) {
ImportRegistryFile "> $($feature.ApplyText)..." $feature.RegistryKey ImportRegistryFile "> $applyText..." $feature.RegistryKey
# Post-import side effects for specific features # Post-import side effects for specific features
switch ($FeatureId) { switch ($FeatureId) {
@@ -32,8 +34,8 @@ function Invoke-FeatureApply {
RemoveApps @('Microsoft.BingSearch') RemoveApps @('Microsoft.BingSearch')
} }
'DisableCopilot' { 'DisableCopilot' {
# Also remove the app package for Copilot # Also remove the app packages for Copilot
RemoveApps @('Microsoft.Copilot') RemoveApps @('Microsoft.Copilot', 'XP9CXNGPPJ97XX')
} }
'DisableTelemetry' { 'DisableTelemetry' {
# Also disable telemetry scheduled tasks # Also disable telemetry scheduled tasks
@@ -44,8 +46,6 @@ function Invoke-FeatureApply {
} }
# ---- Custom features (no registry backing, or special handling required) ---- # ---- Custom features (no registry backing, or special handling required) ----
# Resolve a safe apply-text fallback in case the feature is missing from Features.json
$applyText = if ($feature -and $feature.ApplyText) { $feature.ApplyText } else { $FeatureId }
switch ($FeatureId) { switch ($FeatureId) {
'RemoveApps' { 'RemoveApps' {
Write-Host "> $applyText for $(GetFriendlyTargetUserName)..." Write-Host "> $applyText for $(GetFriendlyTargetUserName)..."
@@ -61,20 +61,6 @@ function Invoke-FeatureApply {
RemoveApps $appsList RemoveApps $appsList
return return
} }
'RemoveAppsCustom' {
Write-Host "> $applyText..."
$appsList = LoadAppsFromFile $script:CustomAppsListFilePath
if ($appsList.Count -eq 0) {
Write-Host "No valid apps were selected for removal" -ForegroundColor Yellow
Write-Host ""
return
}
Write-Host "$($appsList.Count) apps selected for removal"
RemoveApps $appsList
return
}
'RemoveGamingApps' { 'RemoveGamingApps' {
$appsList = @('Microsoft.GamingApp', 'Microsoft.XboxGameOverlay', 'Microsoft.XboxGamingOverlay') $appsList = @('Microsoft.GamingApp', 'Microsoft.XboxGameOverlay', 'Microsoft.XboxGamingOverlay')
Write-Host "> $applyText..." Write-Host "> $applyText..."

View File

@@ -2,7 +2,7 @@
function LoadAppsDetailsFromJson { function LoadAppsDetailsFromJson {
param ( param (
[switch]$OnlyInstalled, [switch]$OnlyInstalled,
[string]$InstalledList = "", [object[]]$InstalledList = $null,
[switch]$InitialCheckedFromJson [switch]$InitialCheckedFromJson
) )
@@ -24,22 +24,19 @@ function LoadAppsDetailsFromJson {
if ($OnlyInstalled) { if ($OnlyInstalled) {
$isInstalled = $false $isInstalled = $false
foreach ($appId in $appIdArray) { foreach ($appId in $appIdArray) {
if (($InstalledList -like ("*$appId*")) -or (Get-AppxPackage -Name $appId)) { # Check Get-AppxPackage first (fast, no process launch)
if (Get-AppxPackage -Name $appId) {
$isInstalled = $true $isInstalled = $true
break break
} }
if (($appId -eq "Microsoft.Edge") -and ($InstalledList -like "* Microsoft.Edge *")) {
$isInstalled = $true # Then check the pre-fetched winget list
break if ($InstalledList -and (Test-AppInWingetList -appId $appId -InstalledList $InstalledList)) {
}
if (($appId -eq "Microsoft.OneDrive") -and (
(Test-Path "$env:ProgramFiles\Microsoft OneDrive\OneDrive.exe") -or
(Test-Path "$env:LOCALAPPDATA\Microsoft\OneDrive\OneDrive.exe")
)) {
$isInstalled = $true $isInstalled = $true
break break
} }
} }
if (-not $isInstalled) { continue } if (-not $isInstalled) { continue }
} }
@@ -59,6 +56,7 @@ function LoadAppsDetailsFromJson {
Description = $appData.Description Description = $appData.Description
SelectedByDefault = $appData.SelectedByDefault SelectedByDefault = $appData.SelectedByDefault
Recommendation = $appData.Recommendation Recommendation = $appData.Recommendation
RemovalMethod = if ($appData.RemovalMethod -and $appData.RemovalMethod -eq 'WinGet') { 'WinGet' } else { 'Appx' }
} }
} }

View File

@@ -1,4 +1,19 @@
# Returns list of apps from the specified file, it trims the app names and removes any comments <#
.SYNOPSIS
Returns a list of app IDs from the specified JSON file.
.DESCRIPTION
Reads an Apps.json file and returns the AppIds for every entry where
SelectedByDefault is $true. Each app entry may declare a single AppId
or an array of AppIds; both forms are handled transparently.
.PARAMETER appsFilePath
Path to a JSON file in the Config/Apps.json format.
.OUTPUTS
System.String[]. An array of app ID strings, or an empty array if the
file does not exist or contains no selected-by-default apps.
#>
function LoadAppsFromFile { function LoadAppsFromFile {
param ( param (
$appsFilePath $appsFilePath
@@ -11,9 +26,6 @@ function LoadAppsFromFile {
} }
try { try {
# Check if file is JSON or text format
if ($appsFilePath -like "*.json") {
# JSON file format
$jsonContent = Get-Content -Path $appsFilePath -Raw | ConvertFrom-Json $jsonContent = Get-Content -Path $appsFilePath -Raw | ConvertFrom-Json
Foreach ($appData in $jsonContent.Apps) { Foreach ($appData in $jsonContent.Apps) {
# Handle AppId as array (could be single or multiple IDs) # Handle AppId as array (could be single or multiple IDs)
@@ -24,19 +36,6 @@ function LoadAppsFromFile {
$appsList += $appIdArray $appsList += $appIdArray
} }
} }
}
else {
# Legacy text file format
Foreach ($app in (Get-Content -Path $appsFilePath | Where-Object { $_ -notmatch '^#.*' -and $_ -notmatch '^\s*$' } )) {
if (-not ($app.IndexOf('#') -eq -1)) {
$app = $app.Substring(0, $app.IndexOf('#'))
}
$app = $app.Trim()
$appString = $app.Trim('*')
$appsList += $appString
}
}
return $appsList return $appsList
} }

View File

@@ -1,20 +0,0 @@
# Saves the provided appsList to the CustomAppsList file
function SaveCustomAppsListToFile {
param (
$appsList
)
if ($script:Params.ContainsKey("WhatIf")) {
Write-Host "[WhatIf] Save custom apps list to file" -ForegroundColor Cyan
return
}
$script:SelectedApps = $appsList
# Create file that stores selected apps if it doesn't exist
if (-not (Test-Path $script:CustomAppsListFilePath)) {
$null = New-Item $script:CustomAppsListFilePath -ItemType File
}
Set-Content -Path $script:CustomAppsListFilePath -Value $script:SelectedApps
}

View File

@@ -311,12 +311,13 @@ function Load-AppsWithList {
[System.Windows.Controls.CheckBox]$OnlyInstalledAppsBox, [System.Windows.Controls.CheckBox]$OnlyInstalledAppsBox,
[System.Windows.Controls.Border]$LoadingAppsIndicator, [System.Windows.Controls.Border]$LoadingAppsIndicator,
[System.Windows.Controls.MenuItem]$ImportConfigBtn, [System.Windows.Controls.MenuItem]$ImportConfigBtn,
[string]$ListOfApps [object[]]$ListOfApps
) )
$script:MainWindowLastSelectedCheckbox = $null $script:MainWindowLastSelectedCheckbox = $null
$loaderScriptPath = $script:LoadAppsDetailsScriptPath $loaderScriptPath = $script:LoadAppsDetailsScriptPath
$helperScriptPath = $script:TestAppInWingetListScriptPath
$appsFilePath = $script:AppsListFilePath $appsFilePath = $script:AppsListFilePath
$onlyInstalled = [bool]$OnlyInstalledAppsBox.IsChecked $onlyInstalled = [bool]$OnlyInstalledAppsBox.IsChecked
@@ -326,13 +327,16 @@ function Load-AppsWithList {
$script:PreloadedAppData = $null $script:PreloadedAppData = $null
} }
else { else {
# Load apps details in a background job to keep the UI responsive # Load apps details in a background job to keep the UI responsive.
# The helper is dot-sourced inside the job because the runspace
# does not inherit the parent scope's dot-sourced functions.
$rawAppData = Invoke-NonBlocking -ScriptBlock { $rawAppData = Invoke-NonBlocking -ScriptBlock {
param($loaderScript, $appsListFilePath, $installedList, $onlyInstalled) param($loaderScript, $helperScript, $appsListFilePath, $installedList, $onlyInstalled)
$script:AppsListFilePath = $appsListFilePath $script:AppsListFilePath = $appsListFilePath
. $helperScript
. $loaderScript . $loaderScript
LoadAppsDetailsFromJson -OnlyInstalled:$onlyInstalled -InstalledList $installedList -InitialCheckedFromJson:$false LoadAppsDetailsFromJson -OnlyInstalled:$onlyInstalled -InstalledList $installedList -InitialCheckedFromJson:$false
} -ArgumentList $loaderScriptPath, $appsFilePath, $ListOfApps, $onlyInstalled } -ArgumentList $loaderScriptPath, $helperScriptPath, $appsFilePath, $ListOfApps, $onlyInstalled
} }
$appsToAdd = @($rawAppData | Where-Object { $_ -and ($_.AppId -or $_.FriendlyName) } | Sort-Object -Property FriendlyName) $appsToAdd = @($rawAppData | Where-Object { $_ -and ($_.AppId -or $_.FriendlyName) } | Sort-Object -Property FriendlyName)
@@ -348,10 +352,9 @@ function Load-AppsWithList {
return return
} }
$brushSafe = [System.Windows.Media.BrushConverter]::new().ConvertFromString('#4CAF50') $brushSafe = $Window.Resources['AppRecommendationSafeColor']
$brushUnsafe = [System.Windows.Media.BrushConverter]::new().ConvertFromString('#F44336') $brushDefault = $Window.Resources['AppRecommendationOptionalColor']
$brushDefault = [System.Windows.Media.BrushConverter]::new().ConvertFromString('#FFC107') $brushUnsafe = $Window.Resources['AppRecommendationUnsafeColor']
$brushSafe.Freeze(); $brushUnsafe.Freeze(); $brushDefault.Freeze()
# Create WPF controls; pump the Dispatcher every batch so the spinner keeps animating. # Create WPF controls; pump the Dispatcher every batch so the spinner keeps animating.
$batchSize = 20 $batchSize = 20
@@ -381,7 +384,7 @@ function Load-AppsWithList {
$dot.ToolTip = switch ($app.Recommendation) { $dot.ToolTip = switch ($app.Recommendation) {
'safe' { '[Recommended] Safe to remove for most users' } 'safe' { '[Recommended] Safe to remove for most users' }
'unsafe' { '[Not Recommended] Only remove if you know what you are doing' } 'unsafe' { '[Not Recommended] Only remove if you know what you are doing' }
default { "[Optional] Remove if you don't need this app" } default { "[Optional] Can be safely removed if you don't need this app" }
} }
[System.Windows.Controls.Grid]::SetColumn($dot, 0) [System.Windows.Controls.Grid]::SetColumn($dot, 0)
@@ -512,7 +515,7 @@ function Load-AppsIntoMainUI {
$Window.Dispatcher.Invoke([System.Windows.Threading.DispatcherPriority]::Render, [action] {}) $Window.Dispatcher.Invoke([System.Windows.Threading.DispatcherPriority]::Render, [action] {})
$Window.Dispatcher.BeginInvoke([System.Windows.Threading.DispatcherPriority]::Background, [action] { $Window.Dispatcher.BeginInvoke([System.Windows.Threading.DispatcherPriority]::Background, [action] {
try { try {
$listOfApps = "" $listOfApps = $null
if ($OnlyInstalledAppsBox.IsChecked -and ($script:WingetInstalled -eq $true)) { if ($OnlyInstalledAppsBox.IsChecked -and ($script:WingetInstalled -eq $true)) {
Write-Host "Retrieving installed apps via winget..." Write-Host "Retrieving installed apps via winget..."

View File

@@ -53,12 +53,12 @@ function Show-AppSelectionWindow {
$window.Dispatcher.Invoke([System.Windows.Threading.DispatcherPriority]::Background, [action]{}) $window.Dispatcher.Invoke([System.Windows.Threading.DispatcherPriority]::Background, [action]{})
$appsPanel.Children.Clear() $appsPanel.Children.Clear()
$listOfApps = "" $listOfApps = $null
if ($onlyInstalledBox.IsChecked -and ($script:WingetInstalled -eq $true)) { if ($onlyInstalledBox.IsChecked -and ($script:WingetInstalled -eq $true)) {
# Attempt to get a list of installed apps via WinGet, times out after 10 seconds # Attempt to get a list of installed apps via WinGet, times out after 10 seconds
$listOfApps = GetInstalledAppsViaWinget -TimeOut 10 $listOfApps = GetInstalledAppsViaWinget -TimeOut 10 -NonBlocking
if (-not $listOfApps) { if ($null -eq $listOfApps) {
# Show error that the script was unable to get list of apps from WinGet # Show error that the script was unable to get list of apps from WinGet
Show-MessageBox -Message 'Unable to load list of installed apps via WinGet.' -Title 'Error' -Button 'OK' -Icon 'Error' -Owner $window | Out-Null Show-MessageBox -Message 'Unable to load list of installed apps via WinGet.' -Title 'Error' -Button 'OK' -Icon 'Error' -Owner $window | Out-Null
$onlyInstalledBox.IsChecked = $false $onlyInstalledBox.IsChecked = $false
@@ -134,7 +134,7 @@ function Show-AppSelectionWindow {
return return
} }
SaveCustomAppsListToFile -appsList $selectedApps $script:SelectedApps = $selectedApps
$window.DialogResult = $true $window.DialogResult = $true
}) })

View File

@@ -940,7 +940,7 @@
# ---- Preload app data ---- # ---- Preload app data ----
try { try {
$script:PreloadedAppData = LoadAppsDetailsFromJson -OnlyInstalled:$false -InstalledList '' -InitialCheckedFromJson:$false $script:PreloadedAppData = LoadAppsDetailsFromJson -OnlyInstalled:$false -InstalledList $null -InitialCheckedFromJson:$false
} }
catch { catch {
Write-Warning "Failed to preload apps list: $_" Write-Warning "Failed to preload apps list: $_"

View File

@@ -7,7 +7,6 @@ param (
[string]$User, [string]$User,
[switch]$NoRestartExplorer, [switch]$NoRestartExplorer,
[switch]$CreateRestorePoint, [switch]$CreateRestorePoint,
[switch]$RunAppsListGenerator,
[switch]$RunDefaults, [switch]$RunDefaults,
[switch]$RunDefaultsLite, [switch]$RunDefaultsLite,
[switch]$RunSavedSettings, [switch]$RunSavedSettings,
@@ -15,7 +14,6 @@ param (
[string]$Apps, [string]$Apps,
[string]$AppRemovalTarget, [string]$AppRemovalTarget,
[switch]$RemoveApps, [switch]$RemoveApps,
[switch]$RemoveAppsCustom,
[switch]$RemoveGamingApps, [switch]$RemoveGamingApps,
[switch]$RemoveHPApps, [switch]$RemoveHPApps,
[switch]$ForceRemoveEdge, [switch]$ForceRemoveEdge,
@@ -152,7 +150,6 @@ if (Test-Path "$configDir") {
New-Item -ItemType Directory -Path "$backupDir" -Force | Out-Null New-Item -ItemType Directory -Path "$backupDir" -Force | Out-Null
$filesToKeep = @( $filesToKeep = @(
'CustomAppsList',
'LastUsedSettings.json' 'LastUsedSettings.json'
) )

View File

@@ -7,7 +7,6 @@ param (
[string]$User, [string]$User,
[switch]$NoRestartExplorer, [switch]$NoRestartExplorer,
[switch]$CreateRestorePoint, [switch]$CreateRestorePoint,
[switch]$RunAppsListGenerator,
[switch]$RunDefaults, [switch]$RunDefaults,
[switch]$RunDefaultsLite, [switch]$RunDefaultsLite,
[switch]$RunSavedSettings, [switch]$RunSavedSettings,
@@ -15,7 +14,6 @@ param (
[string]$Apps, [string]$Apps,
[string]$AppRemovalTarget, [string]$AppRemovalTarget,
[switch]$RemoveApps, [switch]$RemoveApps,
[switch]$RemoveAppsCustom,
[switch]$RemoveGamingApps, [switch]$RemoveGamingApps,
[switch]$RemoveHPApps, [switch]$RemoveHPApps,
[switch]$ForceRemoveEdge, [switch]$ForceRemoveEdge,
@@ -153,7 +151,6 @@ if (Test-Path "$configDir") {
New-Item -ItemType Directory -Path "$backupDir" -Force | Out-Null New-Item -ItemType Directory -Path "$backupDir" -Force | Out-Null
$filesToKeep = @( $filesToKeep = @(
'CustomAppsList',
'LastUsedSettings.json' 'LastUsedSettings.json'
) )

View File

@@ -7,7 +7,6 @@ param (
[string]$User, [string]$User,
[switch]$NoRestartExplorer, [switch]$NoRestartExplorer,
[switch]$CreateRestorePoint, [switch]$CreateRestorePoint,
[switch]$RunAppsListGenerator,
[switch]$RunDefaults, [switch]$RunDefaults,
[switch]$RunDefaultsLite, [switch]$RunDefaultsLite,
[switch]$RunSavedSettings, [switch]$RunSavedSettings,
@@ -15,7 +14,6 @@ param (
[string]$Apps, [string]$Apps,
[string]$AppRemovalTarget, [string]$AppRemovalTarget,
[switch]$RemoveApps, [switch]$RemoveApps,
[switch]$RemoveAppsCustom,
[switch]$RemoveGamingApps, [switch]$RemoveGamingApps,
[switch]$RemoveHPApps, [switch]$RemoveHPApps,
[switch]$ForceRemoveEdge, [switch]$ForceRemoveEdge,
@@ -149,7 +147,6 @@ $script:AppsListFilePath = Join-Path $configPath 'Apps.json'
$script:DefaultSettingsFilePath = Join-Path $configPath 'DefaultSettings.json' $script:DefaultSettingsFilePath = Join-Path $configPath 'DefaultSettings.json'
$script:FeaturesFilePath = Join-Path $configPath 'Features.json' $script:FeaturesFilePath = Join-Path $configPath 'Features.json'
$script:SavedSettingsFilePath = Join-Path $configPath 'LastUsedSettings.json' $script:SavedSettingsFilePath = Join-Path $configPath 'LastUsedSettings.json'
$script:CustomAppsListFilePath = Join-Path $configPath 'CustomAppsList'
$script:DefaultLogPath = Join-Path $logsPath 'Win11Debloat.log' $script:DefaultLogPath = Join-Path $logsPath 'Win11Debloat.log'
$script:RegfilesPath = Join-Path $PSScriptRoot 'Regfiles' $script:RegfilesPath = Join-Path $PSScriptRoot 'Regfiles'
$script:RegistryBackupsPath = Join-Path $PSScriptRoot 'Backups' $script:RegistryBackupsPath = Join-Path $PSScriptRoot 'Backups'
@@ -164,8 +161,9 @@ $script:BubbleHintSchema = Join-Path $schemasPath 'BubbleHint.xaml'
$script:ImportExportConfigSchema = Join-Path $schemasPath 'ImportExportConfigWindow.xaml' $script:ImportExportConfigSchema = Join-Path $schemasPath 'ImportExportConfigWindow.xaml'
$script:RestoreBackupWindowSchema = Join-Path $schemasPath 'RestoreBackupWindow.xaml' $script:RestoreBackupWindowSchema = Join-Path $schemasPath 'RestoreBackupWindow.xaml'
$script:LoadAppsDetailsScriptPath = Join-Path (Join-Path $scriptsPath 'FileIO') 'LoadAppsDetailsFromJson.ps1' $script:LoadAppsDetailsScriptPath = Join-Path (Join-Path $scriptsPath 'FileIO') 'LoadAppsDetailsFromJson.ps1'
$script:TestAppInWingetListScriptPath = Join-Path (Join-Path $scriptsPath 'AppRemoval') 'Test-AppInWingetList.ps1'
$script:ControlParams = 'WhatIf', 'Confirm', 'Verbose', 'Debug', 'LogPath', 'Silent', 'Sysprep', 'User', 'NoRestartExplorer', 'RunDefaults', 'RunDefaultsLite', 'RunSavedSettings', 'Config', 'RunAppsListGenerator', 'CLI', 'AppRemovalTarget' $script:ControlParams = 'WhatIf', 'Confirm', 'Verbose', 'Debug', 'LogPath', 'Silent', 'Sysprep', 'User', 'NoRestartExplorer', 'RunDefaults', 'RunDefaultsLite', 'RunSavedSettings', 'Config', 'CLI', 'AppRemovalTarget'
# Script-level variables for GUI elements # Script-level variables for GUI elements
$script:GuiWindow = $null $script:GuiWindow = $null
@@ -289,6 +287,7 @@ if (-not $script:WingetInstalled -and -not $Silent) {
. "$PSScriptRoot/Scripts/AppRemoval/ForceRemoveEdge.ps1" . "$PSScriptRoot/Scripts/AppRemoval/ForceRemoveEdge.ps1"
. "$PSScriptRoot/Scripts/AppRemoval/RemoveApps.ps1" . "$PSScriptRoot/Scripts/AppRemoval/RemoveApps.ps1"
. "$PSScriptRoot/Scripts/AppRemoval/GetInstalledAppsViaWinget.ps1" . "$PSScriptRoot/Scripts/AppRemoval/GetInstalledAppsViaWinget.ps1"
. "$PSScriptRoot/Scripts/AppRemoval/Test-AppInWingetList.ps1"
# CLI functions # CLI functions
. "$PSScriptRoot/Scripts/CLI/AwaitKeyToExit.ps1" . "$PSScriptRoot/Scripts/CLI/AwaitKeyToExit.ps1"
@@ -322,7 +321,6 @@ if (-not $script:WingetInstalled -and -not $Silent) {
. "$PSScriptRoot/Scripts/FileIO/SaveToFile.ps1" . "$PSScriptRoot/Scripts/FileIO/SaveToFile.ps1"
. "$PSScriptRoot/Scripts/FileIO/SaveSettings.ps1" . "$PSScriptRoot/Scripts/FileIO/SaveSettings.ps1"
. "$PSScriptRoot/Scripts/FileIO/LoadSettings.ps1" . "$PSScriptRoot/Scripts/FileIO/LoadSettings.ps1"
. "$PSScriptRoot/Scripts/FileIO/SaveCustomAppsListToFile.ps1"
. "$PSScriptRoot/Scripts/FileIO/ValidateAppslist.ps1" . "$PSScriptRoot/Scripts/FileIO/ValidateAppslist.ps1"
. "$PSScriptRoot/Scripts/FileIO/LoadAppsFromFile.ps1" . "$PSScriptRoot/Scripts/FileIO/LoadAppsFromFile.ps1"
. "$PSScriptRoot/Scripts/FileIO/LoadAppsDetailsFromJson.ps1" . "$PSScriptRoot/Scripts/FileIO/LoadAppsDetailsFromJson.ps1"
@@ -448,24 +446,6 @@ if ((Test-Path $script:SavedSettingsFilePath) -and ([String]::IsNullOrWhiteSpace
# Default to CLI mode for deployment-targeted parameters. # Default to CLI mode for deployment-targeted parameters.
$launchInCLI = $CLI -or $script:Params.ContainsKey("User") -or $script:Params.ContainsKey("Sysprep") -or $script:Params.ContainsKey("AppRemovalTarget") $launchInCLI = $CLI -or $script:Params.ContainsKey("User") -or $script:Params.ContainsKey("Sysprep") -or $script:Params.ContainsKey("AppRemovalTarget")
# Only run the app selection form if the 'RunAppsListGenerator' parameter was passed to the script
if ($RunAppsListGenerator) {
PrintHeader "Custom Apps List Generator"
$result = Show-AppSelectionWindow
# Show different message based on whether the app selection was saved or cancelled
if ($result -ne $true) {
Write-Host "Application selection window was closed without saving." -ForegroundColor Red
}
else {
Write-Output "Your app selection was saved to the 'CustomAppsList' file, found at:"
Write-Host "$PSScriptRoot" -ForegroundColor Yellow
}
AwaitKeyToExit
}
# Change script execution based on provided parameters or user input # Change script execution based on provided parameters or user input
if ((-not $script:Params.Count) -or $RunDefaults -or $RunDefaultsLite -or $RunSavedSettings -or $Config -or ($controlParamsCount -eq $script:Params.Count)) { if ((-not $script:Params.Count) -or $RunDefaults -or $RunDefaultsLite -or $RunSavedSettings -or $Config -or ($controlParamsCount -eq $script:Params.Count)) {
if ($RunDefaults -or $RunDefaultsLite) { if ($RunDefaults -or $RunDefaultsLite) {