mirror of
https://github.com/Raphire/Win11Debloat.git
synced 2026-07-02 22:58:34 +00:00
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:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,7 +1,3 @@
|
||||
LastSettings
|
||||
SavedSettings
|
||||
LastUsedSettings.json
|
||||
CustomAppsList
|
||||
Logs/*
|
||||
Win11Debloat.log
|
||||
Backups/*
|
||||
493
Config/Apps.json
493
Config/Apps.json
File diff suppressed because it is too large
Load Diff
@@ -339,18 +339,6 @@
|
||||
"MinVersion": 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",
|
||||
"Label": "Remove the Xbox App and Xbox Gamebar",
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -184,6 +184,11 @@
|
||||
<SolidColorBrush x:Key="ValidationErrorColor" Color="#c42b1c"/>
|
||||
<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 -->
|
||||
<Style x:Key="TitleBarButton" TargetType="Button">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
@@ -703,10 +708,32 @@
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<!-- Status Info -->
|
||||
<StackPanel Grid.Row="1" Orientation="Horizontal" Margin="20,0,20,0">
|
||||
<TextBlock x:Name="AppSelectionStatus" Text="" Foreground="{DynamicResource AppFgColor}" Margin="10,0,0,5" HorizontalAlignment="Left"/>
|
||||
<!-- Status Info & Color Legend -->
|
||||
<Grid Grid.Row="1" Margin="20,0">
|
||||
<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>
|
||||
<!-- 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>
|
||||
</DockPanel>
|
||||
</TabItem>
|
||||
|
||||
@@ -1,6 +1,22 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Returns a list of installed apps from winget as structured objects.
|
||||
|
||||
# Run winget list and return installed apps.
|
||||
# Use -NonBlocking to keep the UI responsive (GUI mode) via Invoke-NonBlocking.
|
||||
.DESCRIPTION
|
||||
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 {
|
||||
param (
|
||||
[int]$TimeOut = 10,
|
||||
@@ -11,13 +27,76 @@ function GetInstalledAppsViaWinget {
|
||||
|
||||
$fetchBlock = {
|
||||
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
|
||||
if ($done) {
|
||||
$result = Receive-Job -Job $job
|
||||
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
|
||||
return $null
|
||||
}
|
||||
|
||||
@@ -4,11 +4,12 @@
|
||||
|
||||
.DESCRIPTION
|
||||
Iterates over the provided list of app identifiers and removes each one.
|
||||
Apps are removed via WinGet (for OneDrive and Microsoft Edge) or via
|
||||
Remove-AppxPackage / Remove-ProvisionedAppxPackage (for all other apps).
|
||||
The target scope is determined by script-level parameters:
|
||||
-Sysprep removes from the OS image for future users; -User targets a
|
||||
specific user; otherwise the current user is targeted.
|
||||
The removal method (winget vs. Appx cmdlets) is determined per-app from
|
||||
Apps.json. Microsoft Edge is deferred to the end of the loop so that all
|
||||
winget attempts run before any force-remove prompt. A scheduled task is
|
||||
only created when the User or Sysprep parameter was passed.
|
||||
After each winget removal, the system is checked to confirm whether the
|
||||
app is still installed before reporting an error.
|
||||
|
||||
.PARAMETER appsList
|
||||
An array of app package identifiers to remove (e.g. 'Microsoft.BingNews').
|
||||
@@ -19,7 +20,6 @@
|
||||
.EXAMPLE
|
||||
RemoveApps -appsList (GenerateAppsList)
|
||||
#>
|
||||
# Removes apps specified during function call based on the target scope.
|
||||
function RemoveApps {
|
||||
param (
|
||||
$appslist
|
||||
@@ -34,109 +34,167 @@ function RemoveApps {
|
||||
return
|
||||
}
|
||||
|
||||
# Determine target from script-level params, defaulting to AllUsers
|
||||
$targetUser = GetTargetUserForAppRemoval
|
||||
|
||||
$appIndex = 0
|
||||
$appCount = @($appsList).Count
|
||||
$appIndex = 0
|
||||
|
||||
$edgeIds = @('Microsoft.Edge', 'XPFFTQ037JWMHS')
|
||||
$edgeUninstallSucceeded = $false
|
||||
$edgeScheduledTaskAdded = $false
|
||||
$edgeAppsInList = @()
|
||||
$wingetRemovedApps = @()
|
||||
|
||||
Foreach ($app in $appsList) {
|
||||
if ($script:CancelRequested) {
|
||||
return
|
||||
}
|
||||
if ($script:CancelRequested) { return }
|
||||
|
||||
$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) {
|
||||
& $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"
|
||||
|
||||
# Use WinGet only to remove OneDrive and Edge
|
||||
if (($app -eq "Microsoft.OneDrive") -or ($edgeIds -contains $app)) {
|
||||
if ($script:WingetInstalled -eq $false) {
|
||||
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 }
|
||||
}
|
||||
if ((Get-AppRemovalMethod $app) -eq 'WinGet') {
|
||||
Remove-WinGetApp -app $app
|
||||
$wingetRemovedApps += $app
|
||||
}
|
||||
else {
|
||||
# Uninstall app via WinGet
|
||||
$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
|
||||
Remove-AppxApp -app $app -targetUser $targetUser
|
||||
}
|
||||
}
|
||||
|
||||
# Prompt immediately after the final selected Edge ID attempt (if all attempts failed)
|
||||
$hasRemainingEdgeIds = $false
|
||||
if ($appIndex -lt $appCount) {
|
||||
$remainingApps = @($appsList)[($appIndex)..($appCount - 1)]
|
||||
$hasRemainingEdgeIds = @($remainingApps | Where-Object { $edgeIds -contains $_ }).Count -gt 0
|
||||
# Remove Microsoft Edge
|
||||
if ($edgeAppsInList.Count -gt 0) {
|
||||
Remove-EdgeApp -edgeAppsInList $edgeAppsInList
|
||||
}
|
||||
|
||||
if (-not $hasRemainingEdgeIds -and -not $edgeUninstallSucceeded) {
|
||||
Write-Host "Unable to uninstall Microsoft Edge via WinGet" -ForegroundColor Red
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
elseif ($wingetFailed) {
|
||||
# Check whether any winget-removed apps are still present, and report errors for each one.
|
||||
if ($wingetRemovedApps.Count -gt 0 -or $edgeAppsInList.Count -gt 0) {
|
||||
$postRemovalList = if ($script:WingetInstalled) { GetInstalledAppsViaWinget -TimeOut 10 -NonBlocking } else { $null }
|
||||
foreach ($app in $wingetRemovedApps) {
|
||||
if (Test-AppStillInstalled -appId $app -InstalledList $postRemovalList) {
|
||||
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 + '*'
|
||||
|
||||
try {
|
||||
switch ($targetUser) {
|
||||
"AllUsers" {
|
||||
# Remove installed app for all existing users, and from OS image
|
||||
Invoke-NonBlocking -ScriptBlock {
|
||||
param($pattern)
|
||||
Get-AppxPackage -Name $pattern -AllUsers | Remove-AppxPackage -AllUsers -ErrorAction Continue
|
||||
@@ -144,14 +202,12 @@ function RemoveApps {
|
||||
} -ArgumentList $appPattern
|
||||
}
|
||||
"CurrentUser" {
|
||||
# Remove installed app for current user only
|
||||
Invoke-NonBlocking -ScriptBlock {
|
||||
param($pattern)
|
||||
Get-AppxPackage -Name $pattern | Remove-AppxPackage -ErrorAction Continue
|
||||
} -ArgumentList $appPattern
|
||||
}
|
||||
default {
|
||||
# Target is a specific username - remove app for that user only
|
||||
Invoke-NonBlocking -ScriptBlock {
|
||||
param($pattern, $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
|
||||
}
|
||||
}
|
||||
43
Scripts/AppRemoval/Test-AppInWingetList.ps1
Normal file
43
Scripts/AppRemoval/Test-AppInWingetList.ps1
Normal 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
|
||||
}
|
||||
@@ -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 {
|
||||
Write-Output "Win11Debloat will make the following changes:"
|
||||
|
||||
@@ -31,19 +44,6 @@ function PrintPendingChanges {
|
||||
Write-Host $appsList -ForegroundColor DarkGray
|
||||
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 {
|
||||
if ($script:Features -and $script:Features.ContainsKey($parameterName)) {
|
||||
$message = $script:Features[$parameterName].Label
|
||||
|
||||
@@ -8,7 +8,8 @@ function ShowCLIAppRemoval {
|
||||
|
||||
if ($result -eq $true) {
|
||||
Write-Output "You have selected $($script:SelectedApps.Count) apps for removal"
|
||||
AddParameter 'RemoveAppsCustom'
|
||||
AddParameter 'RemoveApps'
|
||||
AddParameter 'Apps' ($script:SelectedApps -join ',')
|
||||
|
||||
SaveSettings
|
||||
|
||||
|
||||
@@ -25,7 +25,8 @@ function ShowCLIDefaultModeOptions {
|
||||
AddParameter 'Apps' 'Default'
|
||||
}
|
||||
'2' {
|
||||
AddParameter 'RemoveAppsCustom'
|
||||
AddParameter 'RemoveApps'
|
||||
AddParameter 'Apps' ($script:SelectedApps -join ',')
|
||||
|
||||
if ($DisableGameBarIntegrationInput) {
|
||||
AddParameter 'DisableDVR'
|
||||
|
||||
@@ -21,9 +21,11 @@ function Invoke-FeatureApply {
|
||||
$feature = $script:Features[$FeatureId]
|
||||
}
|
||||
|
||||
$applyText = if ($feature -and $feature.ApplyText) { $feature.ApplyText } else { $FeatureId }
|
||||
|
||||
# ---- Registry-backed features: import .reg file, then handle side effects ----
|
||||
if ($feature -and $feature.RegistryKey -and $feature.ApplyText) {
|
||||
ImportRegistryFile "> $($feature.ApplyText)..." $feature.RegistryKey
|
||||
if ($feature -and $feature.RegistryKey) {
|
||||
ImportRegistryFile "> $applyText..." $feature.RegistryKey
|
||||
|
||||
# Post-import side effects for specific features
|
||||
switch ($FeatureId) {
|
||||
@@ -32,8 +34,8 @@ function Invoke-FeatureApply {
|
||||
RemoveApps @('Microsoft.BingSearch')
|
||||
}
|
||||
'DisableCopilot' {
|
||||
# Also remove the app package for Copilot
|
||||
RemoveApps @('Microsoft.Copilot')
|
||||
# Also remove the app packages for Copilot
|
||||
RemoveApps @('Microsoft.Copilot', 'XP9CXNGPPJ97XX')
|
||||
}
|
||||
'DisableTelemetry' {
|
||||
# Also disable telemetry scheduled tasks
|
||||
@@ -44,8 +46,6 @@ function Invoke-FeatureApply {
|
||||
}
|
||||
|
||||
# ---- 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) {
|
||||
'RemoveApps' {
|
||||
Write-Host "> $applyText for $(GetFriendlyTargetUserName)..."
|
||||
@@ -61,20 +61,6 @@ function Invoke-FeatureApply {
|
||||
RemoveApps $appsList
|
||||
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' {
|
||||
$appsList = @('Microsoft.GamingApp', 'Microsoft.XboxGameOverlay', 'Microsoft.XboxGamingOverlay')
|
||||
Write-Host "> $applyText..."
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
function LoadAppsDetailsFromJson {
|
||||
param (
|
||||
[switch]$OnlyInstalled,
|
||||
[string]$InstalledList = "",
|
||||
[object[]]$InstalledList = $null,
|
||||
[switch]$InitialCheckedFromJson
|
||||
)
|
||||
|
||||
@@ -24,22 +24,19 @@ function LoadAppsDetailsFromJson {
|
||||
if ($OnlyInstalled) {
|
||||
$isInstalled = $false
|
||||
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
|
||||
break
|
||||
}
|
||||
if (($appId -eq "Microsoft.Edge") -and ($InstalledList -like "* Microsoft.Edge *")) {
|
||||
$isInstalled = $true
|
||||
break
|
||||
}
|
||||
if (($appId -eq "Microsoft.OneDrive") -and (
|
||||
(Test-Path "$env:ProgramFiles\Microsoft OneDrive\OneDrive.exe") -or
|
||||
(Test-Path "$env:LOCALAPPDATA\Microsoft\OneDrive\OneDrive.exe")
|
||||
)) {
|
||||
|
||||
# Then check the pre-fetched winget list
|
||||
if ($InstalledList -and (Test-AppInWingetList -appId $appId -InstalledList $InstalledList)) {
|
||||
$isInstalled = $true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $isInstalled) { continue }
|
||||
}
|
||||
|
||||
@@ -59,6 +56,7 @@ function LoadAppsDetailsFromJson {
|
||||
Description = $appData.Description
|
||||
SelectedByDefault = $appData.SelectedByDefault
|
||||
Recommendation = $appData.Recommendation
|
||||
RemovalMethod = if ($appData.RemovalMethod -and $appData.RemovalMethod -eq 'WinGet') { 'WinGet' } else { 'Appx' }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
param (
|
||||
$appsFilePath
|
||||
@@ -11,9 +26,6 @@ function LoadAppsFromFile {
|
||||
}
|
||||
|
||||
try {
|
||||
# Check if file is JSON or text format
|
||||
if ($appsFilePath -like "*.json") {
|
||||
# JSON file format
|
||||
$jsonContent = Get-Content -Path $appsFilePath -Raw | ConvertFrom-Json
|
||||
Foreach ($appData in $jsonContent.Apps) {
|
||||
# Handle AppId as array (could be single or multiple IDs)
|
||||
@@ -24,19 +36,6 @@ function LoadAppsFromFile {
|
||||
$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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -311,12 +311,13 @@ function Load-AppsWithList {
|
||||
[System.Windows.Controls.CheckBox]$OnlyInstalledAppsBox,
|
||||
[System.Windows.Controls.Border]$LoadingAppsIndicator,
|
||||
[System.Windows.Controls.MenuItem]$ImportConfigBtn,
|
||||
[string]$ListOfApps
|
||||
[object[]]$ListOfApps
|
||||
)
|
||||
|
||||
$script:MainWindowLastSelectedCheckbox = $null
|
||||
|
||||
$loaderScriptPath = $script:LoadAppsDetailsScriptPath
|
||||
$helperScriptPath = $script:TestAppInWingetListScriptPath
|
||||
$appsFilePath = $script:AppsListFilePath
|
||||
$onlyInstalled = [bool]$OnlyInstalledAppsBox.IsChecked
|
||||
|
||||
@@ -326,13 +327,16 @@ function Load-AppsWithList {
|
||||
$script:PreloadedAppData = $null
|
||||
}
|
||||
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 {
|
||||
param($loaderScript, $appsListFilePath, $installedList, $onlyInstalled)
|
||||
param($loaderScript, $helperScript, $appsListFilePath, $installedList, $onlyInstalled)
|
||||
$script:AppsListFilePath = $appsListFilePath
|
||||
. $helperScript
|
||||
. $loaderScript
|
||||
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)
|
||||
@@ -348,10 +352,9 @@ function Load-AppsWithList {
|
||||
return
|
||||
}
|
||||
|
||||
$brushSafe = [System.Windows.Media.BrushConverter]::new().ConvertFromString('#4CAF50')
|
||||
$brushUnsafe = [System.Windows.Media.BrushConverter]::new().ConvertFromString('#F44336')
|
||||
$brushDefault = [System.Windows.Media.BrushConverter]::new().ConvertFromString('#FFC107')
|
||||
$brushSafe.Freeze(); $brushUnsafe.Freeze(); $brushDefault.Freeze()
|
||||
$brushSafe = $Window.Resources['AppRecommendationSafeColor']
|
||||
$brushDefault = $Window.Resources['AppRecommendationOptionalColor']
|
||||
$brushUnsafe = $Window.Resources['AppRecommendationUnsafeColor']
|
||||
|
||||
# Create WPF controls; pump the Dispatcher every batch so the spinner keeps animating.
|
||||
$batchSize = 20
|
||||
@@ -381,7 +384,7 @@ function Load-AppsWithList {
|
||||
$dot.ToolTip = switch ($app.Recommendation) {
|
||||
'safe' { '[Recommended] Safe to remove for most users' }
|
||||
'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)
|
||||
|
||||
@@ -512,7 +515,7 @@ function Load-AppsIntoMainUI {
|
||||
$Window.Dispatcher.Invoke([System.Windows.Threading.DispatcherPriority]::Render, [action] {})
|
||||
$Window.Dispatcher.BeginInvoke([System.Windows.Threading.DispatcherPriority]::Background, [action] {
|
||||
try {
|
||||
$listOfApps = ""
|
||||
$listOfApps = $null
|
||||
|
||||
if ($OnlyInstalledAppsBox.IsChecked -and ($script:WingetInstalled -eq $true)) {
|
||||
Write-Host "Retrieving installed apps via winget..."
|
||||
|
||||
@@ -53,12 +53,12 @@ function Show-AppSelectionWindow {
|
||||
$window.Dispatcher.Invoke([System.Windows.Threading.DispatcherPriority]::Background, [action]{})
|
||||
|
||||
$appsPanel.Children.Clear()
|
||||
$listOfApps = ""
|
||||
$listOfApps = $null
|
||||
|
||||
if ($onlyInstalledBox.IsChecked -and ($script:WingetInstalled -eq $true)) {
|
||||
# Attempt to get a list of installed apps via WinGet, times out after 10 seconds
|
||||
$listOfApps = GetInstalledAppsViaWinget -TimeOut 10
|
||||
if (-not $listOfApps) {
|
||||
$listOfApps = GetInstalledAppsViaWinget -TimeOut 10 -NonBlocking
|
||||
if ($null -eq $listOfApps) {
|
||||
# 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
|
||||
$onlyInstalledBox.IsChecked = $false
|
||||
@@ -134,7 +134,7 @@ function Show-AppSelectionWindow {
|
||||
return
|
||||
}
|
||||
|
||||
SaveCustomAppsListToFile -appsList $selectedApps
|
||||
$script:SelectedApps = $selectedApps
|
||||
|
||||
$window.DialogResult = $true
|
||||
})
|
||||
|
||||
@@ -940,7 +940,7 @@
|
||||
|
||||
# ---- Preload app data ----
|
||||
try {
|
||||
$script:PreloadedAppData = LoadAppsDetailsFromJson -OnlyInstalled:$false -InstalledList '' -InitialCheckedFromJson:$false
|
||||
$script:PreloadedAppData = LoadAppsDetailsFromJson -OnlyInstalled:$false -InstalledList $null -InitialCheckedFromJson:$false
|
||||
}
|
||||
catch {
|
||||
Write-Warning "Failed to preload apps list: $_"
|
||||
|
||||
@@ -7,7 +7,6 @@ param (
|
||||
[string]$User,
|
||||
[switch]$NoRestartExplorer,
|
||||
[switch]$CreateRestorePoint,
|
||||
[switch]$RunAppsListGenerator,
|
||||
[switch]$RunDefaults,
|
||||
[switch]$RunDefaultsLite,
|
||||
[switch]$RunSavedSettings,
|
||||
@@ -15,7 +14,6 @@ param (
|
||||
[string]$Apps,
|
||||
[string]$AppRemovalTarget,
|
||||
[switch]$RemoveApps,
|
||||
[switch]$RemoveAppsCustom,
|
||||
[switch]$RemoveGamingApps,
|
||||
[switch]$RemoveHPApps,
|
||||
[switch]$ForceRemoveEdge,
|
||||
@@ -152,7 +150,6 @@ if (Test-Path "$configDir") {
|
||||
New-Item -ItemType Directory -Path "$backupDir" -Force | Out-Null
|
||||
|
||||
$filesToKeep = @(
|
||||
'CustomAppsList',
|
||||
'LastUsedSettings.json'
|
||||
)
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ param (
|
||||
[string]$User,
|
||||
[switch]$NoRestartExplorer,
|
||||
[switch]$CreateRestorePoint,
|
||||
[switch]$RunAppsListGenerator,
|
||||
[switch]$RunDefaults,
|
||||
[switch]$RunDefaultsLite,
|
||||
[switch]$RunSavedSettings,
|
||||
@@ -15,7 +14,6 @@ param (
|
||||
[string]$Apps,
|
||||
[string]$AppRemovalTarget,
|
||||
[switch]$RemoveApps,
|
||||
[switch]$RemoveAppsCustom,
|
||||
[switch]$RemoveGamingApps,
|
||||
[switch]$RemoveHPApps,
|
||||
[switch]$ForceRemoveEdge,
|
||||
@@ -153,7 +151,6 @@ if (Test-Path "$configDir") {
|
||||
New-Item -ItemType Directory -Path "$backupDir" -Force | Out-Null
|
||||
|
||||
$filesToKeep = @(
|
||||
'CustomAppsList',
|
||||
'LastUsedSettings.json'
|
||||
)
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ param (
|
||||
[string]$User,
|
||||
[switch]$NoRestartExplorer,
|
||||
[switch]$CreateRestorePoint,
|
||||
[switch]$RunAppsListGenerator,
|
||||
[switch]$RunDefaults,
|
||||
[switch]$RunDefaultsLite,
|
||||
[switch]$RunSavedSettings,
|
||||
@@ -15,7 +14,6 @@ param (
|
||||
[string]$Apps,
|
||||
[string]$AppRemovalTarget,
|
||||
[switch]$RemoveApps,
|
||||
[switch]$RemoveAppsCustom,
|
||||
[switch]$RemoveGamingApps,
|
||||
[switch]$RemoveHPApps,
|
||||
[switch]$ForceRemoveEdge,
|
||||
@@ -149,7 +147,6 @@ $script:AppsListFilePath = Join-Path $configPath 'Apps.json'
|
||||
$script:DefaultSettingsFilePath = Join-Path $configPath 'DefaultSettings.json'
|
||||
$script:FeaturesFilePath = Join-Path $configPath 'Features.json'
|
||||
$script:SavedSettingsFilePath = Join-Path $configPath 'LastUsedSettings.json'
|
||||
$script:CustomAppsListFilePath = Join-Path $configPath 'CustomAppsList'
|
||||
$script:DefaultLogPath = Join-Path $logsPath 'Win11Debloat.log'
|
||||
$script:RegfilesPath = Join-Path $PSScriptRoot 'Regfiles'
|
||||
$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:RestoreBackupWindowSchema = Join-Path $schemasPath 'RestoreBackupWindow.xaml'
|
||||
$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:GuiWindow = $null
|
||||
@@ -289,6 +287,7 @@ if (-not $script:WingetInstalled -and -not $Silent) {
|
||||
. "$PSScriptRoot/Scripts/AppRemoval/ForceRemoveEdge.ps1"
|
||||
. "$PSScriptRoot/Scripts/AppRemoval/RemoveApps.ps1"
|
||||
. "$PSScriptRoot/Scripts/AppRemoval/GetInstalledAppsViaWinget.ps1"
|
||||
. "$PSScriptRoot/Scripts/AppRemoval/Test-AppInWingetList.ps1"
|
||||
|
||||
# CLI functions
|
||||
. "$PSScriptRoot/Scripts/CLI/AwaitKeyToExit.ps1"
|
||||
@@ -322,7 +321,6 @@ if (-not $script:WingetInstalled -and -not $Silent) {
|
||||
. "$PSScriptRoot/Scripts/FileIO/SaveToFile.ps1"
|
||||
. "$PSScriptRoot/Scripts/FileIO/SaveSettings.ps1"
|
||||
. "$PSScriptRoot/Scripts/FileIO/LoadSettings.ps1"
|
||||
. "$PSScriptRoot/Scripts/FileIO/SaveCustomAppsListToFile.ps1"
|
||||
. "$PSScriptRoot/Scripts/FileIO/ValidateAppslist.ps1"
|
||||
. "$PSScriptRoot/Scripts/FileIO/LoadAppsFromFile.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.
|
||||
$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
|
||||
if ((-not $script:Params.Count) -or $RunDefaults -or $RunDefaultsLite -or $RunSavedSettings -or $Config -or ($controlParamsCount -eq $script:Params.Count)) {
|
||||
if ($RunDefaults -or $RunDefaultsLite) {
|
||||
|
||||
Reference in New Issue
Block a user