2022-10-04 21:10:29 +02:00
# Requires -RunAsAdministrator
2020-11-07 02:57:38 +01:00
[ CmdletBinding ( SupportsShouldProcess ) ]
2024-01-31 22:19:50 +01:00
param (
2026-02-01 01:41:12 +01:00
[ switch ] $CLI ,
2024-03-11 23:38:19 +01:00
[ switch ] $Silent ,
2024-06-27 23:08:43 +02:00
[ switch ] $Sysprep ,
2025-05-10 13:02:48 +02:00
[ string ] $LogPath ,
2025-03-05 23:39:29 +01:00
[ string ] $User ,
2025-12-29 01:03:14 +01:00
[ switch ] $NoRestartExplorer ,
2025-05-03 17:55:31 +02:00
[ switch ] $CreateRestorePoint ,
2026-02-01 01:41:12 +01:00
[ switch ] $RunAppsListGenerator ,
2025-09-13 23:36:38 +02:00
[ switch ] $RunDefaults ,
[ switch ] $RunDefaultsLite ,
2025-01-09 21:33:28 +01:00
[ switch ] $RunSavedSettings ,
2025-12-26 20:36:51 +01:00
[ string ] $Apps ,
[ switch ] $RemoveApps ,
2024-03-11 23:38:19 +01:00
[ switch ] $RemoveAppsCustom ,
[ switch ] $RemoveGamingApps ,
[ switch ] $RemoveCommApps ,
2025-05-04 12:27:19 -04:00
[ switch ] $RemoveHPApps ,
2024-03-11 23:38:19 +01:00
[ switch ] $RemoveW11Outlook ,
2024-07-02 18:51:00 +02:00
[ switch ] $ForceRemoveEdge ,
2024-05-24 14:13:15 +02:00
[ switch ] $DisableDVR ,
2025-11-29 17:15:12 +01:00
[ switch ] $DisableGameBarIntegration ,
2024-03-11 23:38:19 +01:00
[ switch ] $DisableTelemetry ,
2025-05-01 22:32:41 +01:00
[ switch ] $DisableFastStartup ,
2026-02-04 13:59:10 +01:00
[ switch ] $DisableBitlockerAutoEncryption ,
2025-08-19 23:24:28 +05:30
[ switch ] $DisableModernStandbyNetworking ,
2026-02-04 14:01:04 +01:00
[ switch ] $DisableUpdateASAP ,
[ switch ] $PreventUpdateAutoReboot ,
[ switch ] $DisableDeliveryOptimization ,
2026-02-01 01:41:12 +01:00
[ switch ] $DisableBing ,
2024-12-26 19:06:49 +01:00
[ switch ] $DisableDesktopSpotlight ,
2026-02-01 01:41:12 +01:00
[ switch ] $DisableLockscreenTips ,
[ switch ] $DisableSuggestions ,
2025-08-16 01:36:00 +02:00
[ switch ] $DisableEdgeAds ,
2026-02-01 01:41:12 +01:00
[ switch ] $DisableBraveBloat ,
2025-05-20 21:55:38 +02:00
[ switch ] $DisableSettings365Ads ,
2025-05-20 21:56:04 +02:00
[ switch ] $DisableSettingsHome ,
2024-03-11 23:38:19 +01:00
[ switch ] $ShowHiddenFolders ,
[ switch ] $ShowKnownFileExt ,
[ switch ] $HideDupliDrive ,
2025-06-12 21:50:57 +02:00
[ switch ] $EnableDarkMode ,
[ switch ] $DisableTransparency ,
[ switch ] $DisableAnimations ,
2024-03-11 23:38:19 +01:00
[ switch ] $TaskbarAlignLeft ,
2025-09-27 14:34:01 +02:00
[ switch ] $CombineTaskbarAlways , [ switch ] $CombineTaskbarWhenFull , [ switch ] $CombineTaskbarNever ,
2025-10-06 23:33:04 +02:00
[ switch ] $CombineMMTaskbarAlways , [ switch ] $CombineMMTaskbarWhenFull , [ switch ] $CombineMMTaskbarNever ,
[ switch ] $MMTaskbarModeAll , [ switch ] $MMTaskbarModeMainActive , [ switch ] $MMTaskbarModeActive ,
2024-03-11 23:38:19 +01:00
[ switch ] $HideSearchTb , [ switch ] $ShowSearchIconTb , [ switch ] $ShowSearchLabelTb , [ switch ] $ShowSearchBoxTb ,
[ switch ] $HideTaskview ,
2025-02-03 21:27:46 +01:00
[ switch ] $DisableStartRecommended ,
2025-06-11 22:04:17 +02:00
[ switch ] $DisableStartPhoneLink ,
2024-03-11 23:38:19 +01:00
[ switch ] $DisableCopilot ,
2024-06-05 09:17:24 +02:00
[ switch ] $DisableRecall ,
2025-09-17 23:33:52 +02:00
[ switch ] $DisableClickToDo ,
2025-08-01 21:21:38 +02:00
[ switch ] $DisablePaintAI ,
[ switch ] $DisableNotepadAI ,
2025-08-16 01:36:00 +02:00
[ switch ] $DisableEdgeAI ,
2026-02-01 01:41:12 +01:00
[ switch ] $DisableWidgets ,
[ switch ] $HideChat ,
2025-04-16 12:31:18 +02:00
[ switch ] $EnableEndTask ,
2025-07-30 15:30:51 -04:00
[ switch ] $EnableLastActiveClick ,
2024-03-11 23:38:19 +01:00
[ switch ] $ClearStart ,
2025-05-19 00:01:49 +02:00
[ string ] $ReplaceStart ,
2024-06-27 23:08:43 +02:00
[ switch ] $ClearStartAllUsers ,
2025-05-19 00:01:49 +02:00
[ string ] $ReplaceStartAllUsers ,
2024-03-11 23:38:19 +01:00
[ switch ] $RevertContextMenu ,
2026-02-01 01:41:12 +01:00
[ switch ] $DisableDragTray ,
2025-02-13 21:08:47 +01:00
[ switch ] $DisableMouseAcceleration ,
2025-04-16 12:56:37 +02:00
[ switch ] $DisableStickyKeys ,
2024-09-04 17:46:41 +02:00
[ switch ] $HideHome ,
2024-06-29 16:18:23 +02:00
[ switch ] $HideGallery ,
2024-10-27 19:28:43 +01:00
[ switch ] $ExplorerToHome ,
[ switch ] $ExplorerToThisPC ,
[ switch ] $ExplorerToDownloads ,
[ switch ] $ExplorerToOneDrive ,
2025-12-29 01:03:14 +01:00
[ switch ] $AddFoldersToThisPC ,
2026-02-01 01:41:12 +01:00
[ switch ] $HideOnedrive ,
[ switch ] $Hide3dObjects ,
[ switch ] $HideMusic ,
[ switch ] $HideIncludeInLibrary ,
[ switch ] $HideGiveAccessTo ,
[ switch ] $HideShare
2020-11-07 02:57:38 +01:00
)
2023-08-03 22:24:27 +02:00
2025-12-15 23:22:29 +01:00
2025-12-26 20:36:51 +01:00
# Define script-level variables & paths
$script:DefaultSettingsFilePath = " $PSScriptRoot /DefaultSettings.json "
2026-02-01 01:41:12 +01:00
$script:AppsListFilePath = " $PSScriptRoot /Apps.json "
2025-12-26 20:36:51 +01:00
$script:SavedSettingsFilePath = " $PSScriptRoot /LastUsedSettings.json "
$script:CustomAppsListFilePath = " $PSScriptRoot /CustomAppsList "
$script:DefaultLogPath = " $PSScriptRoot /Win11Debloat.log "
$script:RegfilesPath = " $PSScriptRoot /Regfiles "
$script:AssetsPath = " $PSScriptRoot /Assets "
2026-02-01 01:41:12 +01:00
$script:AppSelectionSchema = " $script:AssetsPath /Schemas/AppSelectionWindow.xaml "
$script:MainWindowSchema = " $script:AssetsPath /Schemas/MainWindow.xaml "
$script:FeaturesFilePath = " $script:AssetsPath /Features.json "
2025-12-26 20:36:51 +01:00
2026-02-01 01:41:12 +01:00
$script:ControlParams = 'WhatIf' , 'Confirm' , 'Verbose' , 'Debug' , 'LogPath' , 'Silent' , 'Sysprep' , 'User' , 'NoRestartExplorer' , 'RunDefaults' , 'RunDefaultsLite' , 'RunSavedSettings' , 'RunAppsListGenerator' , 'CLI'
# Script-level variables for GUI elements
$script:GuiConsoleOutput = $null
$script:GuiConsoleScrollViewer = $null
$script:GuiWindow = $null
2026-02-06 23:44:39 +01:00
$script:CancelRequested = $false
2025-12-26 20:36:51 +01:00
# Check if current powershell environment is limited by security policies
2024-06-26 20:27:25 +02:00
if ( $ExecutionContext . SessionState . LanguageMode -ne " FullLanguage " ) {
2025-12-26 20:36:51 +01:00
Write-Error " Win11Debloat is unable to run on your system, powershell execution is restricted by security policies "
Write-Output " Press any key to exit... "
$null = [ System.Console ] :: ReadKey ( )
Exit
}
2026-02-01 01:41:12 +01:00
# Display ASCII art launch logo in CLI
Clear-Host
Write-Host " "
Write-Host " "
Write-Host " " -NoNewline ; Write-Host " ^ " -ForegroundColor Blue
Write-Host " " -NoNewline ; Write-Host " / \ " -ForegroundColor Blue
Write-Host " " -NoNewline ; Write-Host " / \ " -ForegroundColor Blue
Write-Host " " -NoNewline ; Write-Host " / \ " -ForegroundColor Blue
Write-Host " " -NoNewline ; Write-Host " / ===== \ " -ForegroundColor Blue
Write-Host " " -NoNewline ; Write-Host " | " -ForegroundColor Blue -NoNewline ; Write-Host " --- " -ForegroundColor White -NoNewline ; Write-Host " | " -ForegroundColor Blue
Write-Host " " -NoNewline ; Write-Host " | " -ForegroundColor Blue -NoNewline ; Write-Host " ( O ) " -ForegroundColor DarkCyan -NoNewline ; Write-Host " | " -ForegroundColor Blue
Write-Host " " -NoNewline ; Write-Host " | " -ForegroundColor Blue -NoNewline ; Write-Host " --- " -ForegroundColor White -NoNewline ; Write-Host " | " -ForegroundColor Blue
Write-Host " " -NoNewline ; Write-Host " | | " -ForegroundColor Blue
Write-Host " " -NoNewline ; Write-Host " /| |\ " -ForegroundColor Blue
Write-Host " " -NoNewline ; Write-Host " / | | \ " -ForegroundColor Blue
Write-Host " " -NoNewline ; Write-Host " | " -ForegroundColor DarkGray -NoNewline ; Write-Host " ''' " -ForegroundColor Red -NoNewline ; Write-Host " | " -ForegroundColor DarkGray -NoNewline ; Write-Host " * " -ForegroundColor Yellow
Write-Host " " -NoNewline ; Write-Host " ( " -ForegroundColor Yellow -NoNewline ; Write-Host " ''' " -ForegroundColor Red -NoNewline ; Write-Host " ) " -ForegroundColor Yellow -NoNewline ; Write-Host " * * " -ForegroundColor DarkYellow
Write-Host " " -NoNewline ; Write-Host " ( " -ForegroundColor DarkYellow -NoNewline ; Write-Host " ' " -ForegroundColor Red -NoNewline ; Write-Host " ) " -ForegroundColor DarkYellow -NoNewline ; Write-Host " * " -ForegroundColor Yellow
Write-Host " "
Write-Host " Win11Debloat is launching... " -ForegroundColor White
Write-Host " Leave this window open " -ForegroundColor DarkGray
Write-Host " "
2024-06-26 20:27:25 +02:00
2025-05-10 13:02:48 +02:00
# Log script output to 'Win11Debloat.log' at the specified path
2025-05-25 20:25:06 +02:00
if ( $LogPath -and ( Test-Path $LogPath ) ) {
2025-05-10 13:02:48 +02:00
Start-Transcript -Path " $LogPath /Win11Debloat.log " -Append -IncludeInvocationHeader -Force | Out-Null
}
else {
2025-12-26 20:36:51 +01:00
Start-Transcript -Path $script:DefaultLogPath -Append -IncludeInvocationHeader -Force | Out-Null
2025-05-10 13:02:48 +02:00
}
2024-06-26 20:27:25 +02:00
2026-02-01 01:41:12 +01:00
# Check if script has all required files
if ( -not ( ( Test-Path $script:DefaultSettingsFilePath ) -and ( Test-Path $script:AppsListFilePath ) -and ( Test-Path $script:RegfilesPath ) -and ( Test-Path $script:AssetsPath ) -and ( Test-Path $script:AppSelectionSchema ) -and ( Test-Path $script:FeaturesFilePath ) ) ) {
Write-Error " Win11Debloat is unable to find required files, please ensure all script files are present "
Write-Output " "
Write-Output " Press any key to exit... "
$null = [ System.Console ] :: ReadKey ( )
Exit
}
# Load feature info from file
$script:Features = @ { }
try {
$featuresData = Get-Content -Path $script:FeaturesFilePath -Raw | ConvertFrom-Json
foreach ( $feature in $featuresData . Features ) {
$script:Features [ $feature . FeatureId ] = $feature
}
}
catch {
Write-Error " Failed to load feature info from Features.json file "
Write-Output " "
Write-Output " Press any key to exit... "
$null = [ System.Console ] :: ReadKey ( )
Exit
}
# Check if WinGet is installed & if it is, check if the version is at least v1.4
try {
if ( [ int ] ( ( ( winget -v ) -replace 'v' , '' ) . split ( '.' ) [ 0 . .1 ] -join '' ) -gt 14 ) {
$script:WingetInstalled = $true
}
else {
$script:WingetInstalled = $false
}
}
catch {
$script:WingetInstalled = $false
}
# Show WinGet warning that requires user confirmation, Suppress confirmation if Silent parameter was passed
if ( -not $script:WingetInstalled -and -not $Silent ) {
Write-Warning " WinGet is not installed or outdated, this may prevent Win11Debloat from removing certain apps "
Write-Output " "
Write-Output " Press any key to continue anyway... "
$null = [ System.Console ] :: ReadKey ( )
}
2025-12-15 23:22:29 +01:00
##################################################################################################################
# #
# FUNCTION DEFINITIONS #
# #
##################################################################################################################
2026-02-01 01:41:12 +01:00
# Writes to both GUI console output and standard console
function Write-ToConsole {
param (
[ string ] $message ,
[ string ] $ForegroundColor = $null
)
if ( $script:GuiConsoleOutput ) {
# GUI mode
$timestamp = Get-Date -Format " HH:mm:ss "
$script:GuiConsoleOutput . Dispatcher . Invoke ( [ System.Windows.Threading.DispatcherPriority ] :: Send , [ action ] {
try {
$runText = " [ $timestamp ] $message `n "
$run = New-Object System . Windows . Documents . Run $runText
if ( $ForegroundColor ) {
try {
$colorObj = [ System.Windows.Media.ColorConverter ] :: ConvertFromString ( $ForegroundColor )
if ( $colorObj ) {
$brush = [ System.Windows.Media.SolidColorBrush ] :: new ( $colorObj )
$run . Foreground = $brush
}
}
catch {
# Invalid color string - ignore and fall back to default
}
}
2024-03-04 00:10:24 +01:00
2026-02-01 01:41:12 +01:00
$script:GuiConsoleOutput . Inlines . Add ( $run )
if ( $script:GuiConsoleScrollViewer ) { $script:GuiConsoleScrollViewer . ScrollToEnd ( ) }
}
catch {
# If any UI update fails, fall back to simple text append
try { $script:GuiConsoleOutput . Text + = " [ $timestamp ] $message `n " } catch { }
}
} )
2024-04-05 23:35:10 +02:00
2026-02-01 01:41:12 +01:00
# Force UI to process pending updates for real-time display
if ( $script:GuiWindow ) {
$script:GuiWindow . Dispatcher . Invoke ( [ System.Windows.Threading.DispatcherPriority ] :: Background , [ action ] { } )
}
}
2025-12-26 20:36:51 +01:00
2026-02-01 01:41:12 +01:00
try {
if ( $ForegroundColor ) {
Write-Host $message -ForegroundColor $ForegroundColor
}
else {
Write-Host $message
2024-09-17 15:06:18 +02:00
}
2026-02-01 01:41:12 +01:00
}
catch {
Write-Host $message
}
}
2024-09-17 15:06:18 +02:00
2024-03-04 00:10:24 +01:00
2026-02-01 01:41:12 +01:00
# Loads a JSON file from the specified path and returns the parsed object
# Returns $null if the file doesn't exist or if parsing fails
function LoadJsonFile {
param (
[ string ] $filePath ,
[ string ] $expectedVersion = $null ,
[ switch ] $optionalFile
)
if ( -not ( Test-Path $filePath ) ) {
if ( -not $optionalFile ) {
Write-Error " File not found: $filePath "
2025-12-26 20:36:51 +01:00
}
2026-02-01 01:41:12 +01:00
return $null
}
try {
$jsonContent = Get-Content -Path $filePath -Raw | ConvertFrom-Json
# Validate version if specified
if ( $expectedVersion -and $jsonContent . Version -and $jsonContent . Version -ne $expectedVersion ) {
Write-Error " $( Split-Path $filePath -Leaf ) version mismatch (expected $expectedVersion , found $( $jsonContent . Version ) ) "
return $null
2025-12-15 23:22:29 +01:00
}
2026-02-01 01:41:12 +01:00
return $jsonContent
}
catch {
Write-Error " Failed to parse JSON file: $filePath "
return $null
}
}
2024-03-04 00:10:24 +01:00
2026-02-01 01:41:12 +01:00
# Loads settings from a JSON file and adds them to script params
# Used by command-line modes (ShowDefaultModeOptions, LoadAndShowLastUsedSettings)
function LoadSettingsToParams {
param (
[ string ] $filePath ,
[ string ] $expectedVersion = " 1.0 "
)
$settingsJson = LoadJsonFile -filePath $filePath -expectedVersion $expectedVersion
if ( -not $settingsJson -or -not $settingsJson . Settings ) {
throw " Failed to load settings from $( Split-Path $filePath -Leaf ) "
2024-03-04 00:10:24 +01:00
}
2026-02-01 01:41:12 +01:00
# Get current Windows build version
$WinVersion = Get-ItemPropertyValue 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' CurrentBuild
foreach ( $setting in $settingsJson . Settings ) {
if ( $setting . Value -eq $false ) {
continue
}
2024-03-04 00:10:24 +01:00
2026-02-01 01:41:12 +01:00
$feature = $script:Features [ $setting . Name ]
# Check version and feature compatibility using Features.json
if ( ( $feature . MinVersion -and $WinVersion -lt $feature . MinVersion ) -or ( $feature . MaxVersion -and $WinVersion -gt $feature . MaxVersion ) -or ( $feature . FeatureId -eq 'DisableModernStandbyNetworking' -and ( -not $script:ModernStandbySupported ) ) ) {
continue
}
AddParameter $setting . Name $setting . Value
2024-04-05 23:35:10 +02:00
}
2026-02-01 01:41:12 +01:00
}
2024-04-05 23:35:10 +02:00
2026-02-01 01:41:12 +01:00
# Applies settings from a JSON object to UI controls (checkboxes and comboboxes)
# Used by LoadDefaultsBtn and LoadLastUsedBtn in the UI
function ApplySettingsToUiControls {
param (
$window ,
$settingsJson ,
$uiControlMappings
)
if ( -not $settingsJson -or -not $settingsJson . Settings ) {
return $false
}
# First, reset all tweaks to "No Change" (index 0) or unchecked
if ( $uiControlMappings ) {
foreach ( $comboName in $uiControlMappings . Keys ) {
$control = $window . FindName ( $comboName )
if ( $control -is [ System.Windows.Controls.CheckBox ] ) {
$control . IsChecked = $false
}
elseif ( $control -is [ System.Windows.Controls.ComboBox ] ) {
$control . SelectedIndex = 0
}
}
}
# Also uncheck RestorePointCheckBox
$restorePointCheckBox = $window . FindName ( 'RestorePointCheckBox' )
if ( $restorePointCheckBox ) {
$restorePointCheckBox . IsChecked = $false
}
# Apply settings from JSON
foreach ( $setting in $settingsJson . Settings ) {
if ( $setting . Value -ne $true ) { continue }
$paramName = $setting . Name
# Handle RestorePointCheckBox separately
if ( $paramName -eq 'CreateRestorePoint' ) {
if ( $restorePointCheckBox ) { $restorePointCheckBox . IsChecked = $true }
continue
}
2024-04-05 23:35:10 +02:00
2026-02-01 01:41:12 +01:00
if ( $uiControlMappings ) {
foreach ( $comboName in $uiControlMappings . Keys ) {
$mapping = $uiControlMappings [ $comboName ]
if ( $mapping . Type -eq 'group' ) {
$i = 1
foreach ( $val in $mapping . Values ) {
if ( $val . FeatureIds -contains $paramName ) {
$control = $window . FindName ( $comboName )
if ( $control -and $control . Visibility -eq 'Visible' ) {
if ( $control -is [ System.Windows.Controls.ComboBox ] ) {
$control . SelectedIndex = $i
}
}
break
2024-04-05 23:35:10 +02:00
}
2026-02-01 01:41:12 +01:00
$i + +
2024-04-05 23:35:10 +02:00
}
2026-02-01 01:41:12 +01:00
}
elseif ( $mapping . Type -eq 'feature' ) {
if ( $mapping . FeatureId -eq $paramName ) {
$control = $window . FindName ( $comboName )
if ( $control -and $control . Visibility -eq 'Visible' ) {
if ( $control -is [ System.Windows.Controls.CheckBox ] ) {
$control . IsChecked = $true
}
elseif ( $control -is [ System.Windows.Controls.ComboBox ] ) {
$control . SelectedIndex = 1
}
2024-04-05 23:35:10 +02:00
}
}
}
}
}
}
2026-02-01 01:41:12 +01:00
return $true
}
2024-04-05 23:35:10 +02:00
2024-03-29 16:56:29 +01:00
2026-02-01 01:41:12 +01:00
# Attaches shift-click selection behavior to a checkbox in an apps panel
# Parameters:
# - $checkbox: The checkbox to attach the behavior to
# - $appsPanel: The StackPanel containing checkbox items
# - $lastSelectedCheckboxRef: A reference to a variable storing the last clicked checkbox
# - $updateStatusCallback: Optional callback to update selection status
function AttachShiftClickBehavior {
param (
[ System.Windows.Controls.CheckBox ] $checkbox ,
[ System.Windows.Controls.StackPanel ] $appsPanel ,
[ ref ] $lastSelectedCheckboxRef ,
[ scriptblock ] $updateStatusCallback = $null
)
2024-04-01 14:07:59 +02:00
2026-02-01 01:41:12 +01:00
# Use a closure to capture the parameters
$checkbox . Add_PreviewMouseLeftButtonDown ( {
param (
$sender ,
$e
)
$isShiftPressed = [ System.Windows.Input.Keyboard ] :: IsKeyDown ( [ System.Windows.Input.Key ] :: LeftShift ) -or
[ System.Windows.Input.Keyboard ] :: IsKeyDown ( [ System.Windows.Input.Key ] :: RightShift )
if ( $isShiftPressed -and $null -ne $lastSelectedCheckboxRef . Value ) {
# Get all visible checkboxes in the panel
$visibleCheckboxes = @ ( )
foreach ( $child in $appsPanel . Children ) {
if ( $child -is [ System.Windows.Controls.CheckBox ] -and $child . Visibility -eq 'Visible' ) {
$visibleCheckboxes + = $child
}
2024-04-01 14:07:59 +02:00
}
2024-03-04 12:56:12 +01:00
2026-02-01 01:41:12 +01:00
# Find indices of the last selected and current checkbox
$lastIndex = -1
$currentIndex = -1
2024-03-04 12:56:12 +01:00
2026-02-01 01:41:12 +01:00
for ( $i = 0 ; $i -lt $visibleCheckboxes . Count ; $i + + ) {
if ( $visibleCheckboxes [ $i ] -eq $lastSelectedCheckboxRef . Value ) {
$lastIndex = $i
}
if ( $visibleCheckboxes [ $i ] -eq $sender ) {
$currentIndex = $i
}
2024-03-04 12:56:12 +01:00
}
2024-08-19 21:27:53 +02:00
2026-02-01 01:41:12 +01:00
if ( $lastIndex -ge 0 -and $currentIndex -ge 0 -and $lastIndex -ne $currentIndex ) {
$startIndex = [ Math ] :: Min ( $lastIndex , $currentIndex )
$endIndex = [ Math ] :: Max ( $lastIndex , $currentIndex )
2025-12-26 20:36:51 +01:00
2026-02-01 01:41:12 +01:00
$shouldDeselect = $sender . IsChecked
2024-03-04 12:56:12 +01:00
2026-02-01 01:41:12 +01:00
# Set all checkboxes in the range to the appropriate state
for ( $i = $startIndex ; $i -le $endIndex ; $i + + ) {
$visibleCheckboxes [ $i ] . IsChecked = -not $shouldDeselect
}
if ( $updateStatusCallback ) {
& $updateStatusCallback
2024-03-04 12:56:12 +01:00
}
2026-02-01 01:41:12 +01:00
# Mark the event as handled to prevent the default toggle behavior
$e . Handled = $true
return
2024-03-04 12:56:12 +01:00
}
}
2025-12-26 20:36:51 +01:00
2026-02-01 01:41:12 +01:00
# Update the last selected checkbox reference for next time
$lastSelectedCheckboxRef . Value = $sender
} . GetNewClosure ( ) )
2024-03-04 00:10:24 +01:00
}
2026-02-01 01:41:12 +01:00
# Sets resource colors for a WPF window based on dark mode preference
function SetWindowThemeResources {
2025-12-26 20:36:51 +01:00
param (
2026-02-01 01:41:12 +01:00
$window ,
[ bool ] $usesDarkMode
2025-12-26 20:36:51 +01:00
)
2026-02-01 01:41:12 +01:00
if ( $usesDarkMode ) {
$window . Resources . Add ( " BgColor " , [ System.Windows.Media.SolidColorBrush ] :: new ( [ System.Windows.Media.ColorConverter ] :: ConvertFromString ( " #202020 " ) ) )
$window . Resources . Add ( " FgColor " , [ System.Windows.Media.SolidColorBrush ] :: new ( [ System.Windows.Media.ColorConverter ] :: ConvertFromString ( " #FFFFFF " ) ) )
$window . Resources . Add ( " CardBgColor " , [ System.Windows.Media.SolidColorBrush ] :: new ( [ System.Windows.Media.ColorConverter ] :: ConvertFromString ( " #2b2b2b " ) ) )
$window . Resources . Add ( " BorderColor " , [ System.Windows.Media.SolidColorBrush ] :: new ( [ System.Windows.Media.ColorConverter ] :: ConvertFromString ( " #404040 " ) ) )
$window . Resources . Add ( " ButtonBorderColor " , [ System.Windows.Media.SolidColorBrush ] :: new ( [ System.Windows.Media.ColorConverter ] :: ConvertFromString ( " #404040 " ) ) )
$window . Resources . Add ( " CheckBoxBgColor " , [ System.Windows.Media.SolidColorBrush ] :: new ( [ System.Windows.Media.ColorConverter ] :: ConvertFromString ( " #272727 " ) ) )
$window . Resources . Add ( " CheckBoxBorderColor " , [ System.Windows.Media.SolidColorBrush ] :: new ( [ System.Windows.Media.ColorConverter ] :: ConvertFromString ( " #808080 " ) ) )
$window . Resources . Add ( " CheckBoxHoverColor " , [ System.Windows.Media.SolidColorBrush ] :: new ( [ System.Windows.Media.ColorConverter ] :: ConvertFromString ( " #343434 " ) ) )
$window . Resources . Add ( " ComboBgColor " , [ System.Windows.Media.SolidColorBrush ] :: new ( [ System.Windows.Media.ColorConverter ] :: ConvertFromString ( " #373737 " ) ) )
$window . Resources . Add ( " ComboHoverColor " , [ System.Windows.Media.SolidColorBrush ] :: new ( [ System.Windows.Media.ColorConverter ] :: ConvertFromString ( " #434343 " ) ) )
$window . Resources . Add ( " ComboItemBgColor " , [ System.Windows.Media.SolidColorBrush ] :: new ( [ System.Windows.Media.ColorConverter ] :: ConvertFromString ( " #2c2c2c " ) ) )
$window . Resources . Add ( " ComboItemHoverColor " , [ System.Windows.Media.SolidColorBrush ] :: new ( [ System.Windows.Media.ColorConverter ] :: ConvertFromString ( " #383838 " ) ) )
$window . Resources . Add ( " ComboItemSelectedColor " , [ System.Windows.Media.SolidColorBrush ] :: new ( [ System.Windows.Media.ColorConverter ] :: ConvertFromString ( " #343434 " ) ) )
$window . Resources . Add ( " AccentColor " , [ System.Windows.Media.SolidColorBrush ] :: new ( [ System.Windows.Media.ColorConverter ] :: ConvertFromString ( " #FFD700 " ) ) )
$window . Resources . Add ( " ButtonDisabled " , [ System.Windows.Media.SolidColorBrush ] :: new ( [ System.Windows.Media.ColorConverter ] :: ConvertFromString ( " #434343 " ) ) )
$window . Resources . Add ( " ButtonTextDisabled " , [ System.Windows.Media.SolidColorBrush ] :: new ( [ System.Windows.Media.ColorConverter ] :: ConvertFromString ( " #989898 " ) ) )
$window . Resources . Add ( " SecondaryButtonBg " , [ System.Windows.Media.SolidColorBrush ] :: new ( [ System.Windows.Media.ColorConverter ] :: ConvertFromString ( " #393939 " ) ) )
$window . Resources . Add ( " SecondaryButtonHover " , [ System.Windows.Media.SolidColorBrush ] :: new ( [ System.Windows.Media.ColorConverter ] :: ConvertFromString ( " #2a2a2a " ) ) )
$window . Resources . Add ( " SecondaryButtonPressed " , [ System.Windows.Media.SolidColorBrush ] :: new ( [ System.Windows.Media.ColorConverter ] :: ConvertFromString ( " #1e1e1e " ) ) )
$window . Resources . Add ( " SecondaryButtonDisabled " , [ System.Windows.Media.SolidColorBrush ] :: new ( [ System.Windows.Media.ColorConverter ] :: ConvertFromString ( " #3b3b3b " ) ) )
$window . Resources . Add ( " SecondaryButtonTextDisabled " , [ System.Windows.Media.SolidColorBrush ] :: new ( [ System.Windows.Media.ColorConverter ] :: ConvertFromString ( " #787878 " ) ) )
$window . Resources . Add ( " ScrollBarThumbColor " , [ System.Windows.Media.SolidColorBrush ] :: new ( [ System.Windows.Media.ColorConverter ] :: ConvertFromString ( " #3d3d3d " ) ) )
$window . Resources . Add ( " ScrollBarThumbHoverColor " , [ System.Windows.Media.SolidColorBrush ] :: new ( [ System.Windows.Media.ColorConverter ] :: ConvertFromString ( " #4b4b4b " ) ) )
2025-12-26 20:36:51 +01:00
}
2026-02-01 01:41:12 +01:00
else {
$window . Resources . Add ( " BgColor " , [ System.Windows.Media.SolidColorBrush ] :: new ( [ System.Windows.Media.ColorConverter ] :: ConvertFromString ( " #f3f3f3 " ) ) )
$window . Resources . Add ( " FgColor " , [ System.Windows.Media.SolidColorBrush ] :: new ( [ System.Windows.Media.ColorConverter ] :: ConvertFromString ( " #000000 " ) ) )
$window . Resources . Add ( " CardBgColor " , [ System.Windows.Media.SolidColorBrush ] :: new ( [ System.Windows.Media.ColorConverter ] :: ConvertFromString ( " #fbfbfb " ) ) )
$window . Resources . Add ( " BorderColor " , [ System.Windows.Media.SolidColorBrush ] :: new ( [ System.Windows.Media.ColorConverter ] :: ConvertFromString ( " #ededed " ) ) )
$window . Resources . Add ( " ButtonBorderColor " , [ System.Windows.Media.SolidColorBrush ] :: new ( [ System.Windows.Media.ColorConverter ] :: ConvertFromString ( " #d3d3d3 " ) ) )
$window . Resources . Add ( " CheckBoxBgColor " , [ System.Windows.Media.SolidColorBrush ] :: new ( [ System.Windows.Media.ColorConverter ] :: ConvertFromString ( " #f5f5f5 " ) ) )
$window . Resources . Add ( " CheckBoxBorderColor " , [ System.Windows.Media.SolidColorBrush ] :: new ( [ System.Windows.Media.ColorConverter ] :: ConvertFromString ( " #898989 " ) ) )
$window . Resources . Add ( " CheckBoxHoverColor " , [ System.Windows.Media.SolidColorBrush ] :: new ( [ System.Windows.Media.ColorConverter ] :: ConvertFromString ( " #ececec " ) ) )
$window . Resources . Add ( " ComboBgColor " , [ System.Windows.Media.SolidColorBrush ] :: new ( [ System.Windows.Media.ColorConverter ] :: ConvertFromString ( " #FFFFFF " ) ) )
$window . Resources . Add ( " ComboHoverColor " , [ System.Windows.Media.SolidColorBrush ] :: new ( [ System.Windows.Media.ColorConverter ] :: ConvertFromString ( " #f8f8f8 " ) ) )
$window . Resources . Add ( " ComboItemBgColor " , [ System.Windows.Media.SolidColorBrush ] :: new ( [ System.Windows.Media.ColorConverter ] :: ConvertFromString ( " #f9f9f9 " ) ) )
$window . Resources . Add ( " ComboItemHoverColor " , [ System.Windows.Media.SolidColorBrush ] :: new ( [ System.Windows.Media.ColorConverter ] :: ConvertFromString ( " #f0f0f0 " ) ) )
$window . Resources . Add ( " ComboItemSelectedColor " , [ System.Windows.Media.SolidColorBrush ] :: new ( [ System.Windows.Media.ColorConverter ] :: ConvertFromString ( " #f3f3f3 " ) ) )
$window . Resources . Add ( " AccentColor " , [ System.Windows.Media.SolidColorBrush ] :: new ( [ System.Windows.Media.ColorConverter ] :: ConvertFromString ( " #ffae00 " ) ) )
$window . Resources . Add ( " ButtonDisabled " , [ System.Windows.Media.SolidColorBrush ] :: new ( [ System.Windows.Media.ColorConverter ] :: ConvertFromString ( " #bfbfbf " ) ) )
$window . Resources . Add ( " ButtonTextDisabled " , [ System.Windows.Media.SolidColorBrush ] :: new ( [ System.Windows.Media.ColorConverter ] :: ConvertFromString ( " #ffffff " ) ) )
$window . Resources . Add ( " SecondaryButtonBg " , [ System.Windows.Media.SolidColorBrush ] :: new ( [ System.Windows.Media.ColorConverter ] :: ConvertFromString ( " #fbfbfb " ) ) )
$window . Resources . Add ( " SecondaryButtonHover " , [ System.Windows.Media.SolidColorBrush ] :: new ( [ System.Windows.Media.ColorConverter ] :: ConvertFromString ( " #f6f6f6 " ) ) )
$window . Resources . Add ( " SecondaryButtonPressed " , [ System.Windows.Media.SolidColorBrush ] :: new ( [ System.Windows.Media.ColorConverter ] :: ConvertFromString ( " #f0f0f0 " ) ) )
$window . Resources . Add ( " SecondaryButtonDisabled " , [ System.Windows.Media.SolidColorBrush ] :: new ( [ System.Windows.Media.ColorConverter ] :: ConvertFromString ( " #f7f7f7 " ) ) )
$window . Resources . Add ( " SecondaryButtonTextDisabled " , [ System.Windows.Media.SolidColorBrush ] :: new ( [ System.Windows.Media.ColorConverter ] :: ConvertFromString ( " #b7b7b7 " ) ) )
$window . Resources . Add ( " ScrollBarThumbColor " , [ System.Windows.Media.SolidColorBrush ] :: new ( [ System.Windows.Media.ColorConverter ] :: ConvertFromString ( " #b9b9b9 " ) ) )
$window . Resources . Add ( " ScrollBarThumbHoverColor " , [ System.Windows.Media.SolidColorBrush ] :: new ( [ System.Windows.Media.ColorConverter ] :: ConvertFromString ( " #8b8b8b " ) ) )
}
$window . Resources . Add ( " ButtonBg " , [ System.Windows.Media.SolidColorBrush ] :: new ( [ System.Windows.Media.ColorConverter ] :: ConvertFromString ( " #0067c0 " ) ) )
$window . Resources . Add ( " ButtonHover " , [ System.Windows.Media.SolidColorBrush ] :: new ( [ System.Windows.Media.ColorConverter ] :: ConvertFromString ( " #1E88E5 " ) ) )
$window . Resources . Add ( " ButtonPressed " , [ System.Windows.Media.SolidColorBrush ] :: new ( [ System.Windows.Media.ColorConverter ] :: ConvertFromString ( " #3284cc " ) ) )
$window . Resources . Add ( " CloseHover " , [ System.Windows.Media.SolidColorBrush ] :: new ( [ System.Windows.Media.ColorConverter ] :: ConvertFromString ( " #c42b1c " ) ) )
}
2025-12-26 20:36:51 +01:00
2026-02-01 01:41:12 +01:00
# Checks if the system is set to use dark mode for apps
function GetSystemUsesDarkMode {
try {
return ( Get-ItemProperty -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize' -Name 'AppsUseLightTheme' ) . AppsUseLightTheme -eq 0
}
catch {
return $false
2025-12-26 20:36:51 +01:00
}
}
2026-02-01 01:41:12 +01:00
# Initializes and opens the main GUI window
function OpenGUI {
2026-02-04 11:34:55 +01:00
Add-Type -AssemblyName PresentationFramework , PresentationCore , WindowsBase , System . Windows . Forms | Out-Null
2023-08-03 01:18:02 +02:00
2026-02-01 01:41:12 +01:00
# Get current Windows build version
$WinVersion = Get-ItemPropertyValue 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' CurrentBuild
2024-03-29 16:56:29 +01:00
2026-02-01 01:41:12 +01:00
$usesDarkMode = GetSystemUsesDarkMode
# Load XAML from file
$xaml = Get-Content -Path $script:MainWindowSchema -Raw
$reader = [ System.Xml.XmlReader ] :: Create ( [ System.IO.StringReader ] :: new ( $xaml ) )
try {
$window = [ System.Windows.Markup.XamlReader ] :: Load ( $reader )
2025-12-26 20:36:51 +01:00
}
2026-02-01 01:41:12 +01:00
finally {
$reader . Close ( )
}
SetWindowThemeResources -window $window -usesDarkMode $usesDarkMode
2025-12-26 20:36:51 +01:00
2026-02-01 01:41:12 +01:00
# Get named elements
$titleBar = $window . FindName ( 'TitleBar' )
$helpBtn = $window . FindName ( 'HelpBtn' )
$closeBtn = $window . FindName ( 'CloseBtn' )
# Title bar event handlers
$titleBar . Add_MouseLeftButtonDown ( {
if ( $_ . OriginalSource -is [ System.Windows.Controls.Grid ] -or $_ . OriginalSource -is [ System.Windows.Controls.Border ] -or $_ . OriginalSource -is [ System.Windows.Controls.TextBlock ] ) {
$window . DragMove ( )
2023-08-02 21:58:09 +02:00
}
2026-02-01 01:41:12 +01:00
} )
$helpBtn . Add_Click ( {
Start-Process " https://github.com/Raphire/Win11Debloat/wiki "
} )
$closeBtn . Add_Click ( {
$window . Close ( )
} )
2026-02-06 23:44:39 +01:00
# Ensure closing the main window stops all execution
2026-02-01 01:41:12 +01:00
$window . Add_Closing ( {
2026-02-06 23:44:39 +01:00
$script:CancelRequested = $true
2026-02-01 01:41:12 +01:00
} )
2026-02-04 11:34:55 +01:00
# Implement window resize functionality
$resizeLeft = $window . FindName ( 'ResizeLeft' )
$resizeRight = $window . FindName ( 'ResizeRight' )
$resizeTop = $window . FindName ( 'ResizeTop' )
$resizeBottom = $window . FindName ( 'ResizeBottom' )
$resizeTopLeft = $window . FindName ( 'ResizeTopLeft' )
$resizeTopRight = $window . FindName ( 'ResizeTopRight' )
$resizeBottomLeft = $window . FindName ( 'ResizeBottomLeft' )
$resizeBottomRight = $window . FindName ( 'ResizeBottomRight' )
$script:resizing = $false
$script:resizeEdges = $null
$script:resizeStart = $null
$script:windowStart = $null
$script:resizeElement = $null
$resizeHandler = {
param ( $sender , $e )
$script:resizing = $true
$script:resizeElement = $sender
$script:resizeStart = [ System.Windows.Forms.Cursor ] :: Position
$script:windowStart = @ {
Left = $window . Left
Top = $window . Top
Width = $window . ActualWidth
Height = $window . ActualHeight
}
# Parse direction tag into edge flags for cleaner resize logic
$direction = $sender . Tag
$script:resizeEdges = @ {
Left = $direction -match 'Left'
Right = $direction -match 'Right'
Top = $direction -match 'Top'
Bottom = $direction -match 'Bottom'
}
$sender . CaptureMouse ( )
$e . Handled = $true
}
$moveHandler = {
param ( $sender , $e )
if ( -not $script:resizing ) { return }
$current = [ System.Windows.Forms.Cursor ] :: Position
$deltaX = $current . X - $script:resizeStart . X
$deltaY = $current . Y - $script:resizeStart . Y
# Handle horizontal resize
if ( $script:resizeEdges . Left ) {
$newWidth = [ Math ] :: Max ( $window . MinWidth , $script:windowStart . Width - $deltaX )
if ( $newWidth -ne $window . Width ) {
$window . Left = $script:windowStart . Left + ( $script:windowStart . Width - $newWidth )
$window . Width = $newWidth
}
}
elseif ( $script:resizeEdges . Right ) {
$window . Width = [ Math ] :: Max ( $window . MinWidth , $script:windowStart . Width + $deltaX )
}
# Handle vertical resize
if ( $script:resizeEdges . Top ) {
$newHeight = [ Math ] :: Max ( $window . MinHeight , $script:windowStart . Height - $deltaY )
if ( $newHeight -ne $window . Height ) {
$window . Top = $script:windowStart . Top + ( $script:windowStart . Height - $newHeight )
$window . Height = $newHeight
}
}
elseif ( $script:resizeEdges . Bottom ) {
$window . Height = [ Math ] :: Max ( $window . MinHeight , $script:windowStart . Height + $deltaY )
}
$e . Handled = $true
}
$releaseHandler = {
param ( $sender , $e )
if ( $script:resizing -and $script:resizeElement ) {
$script:resizing = $false
$script:resizeEdges = $null
$script:resizeElement . ReleaseMouseCapture ( )
$script:resizeElement = $null
$e . Handled = $true
}
}
# Set tags and add event handlers for resize borders
$resizeLeft . Tag = 'Left'
$resizeLeft . Add_PreviewMouseLeftButtonDown ( $resizeHandler )
$resizeLeft . Add_MouseMove ( $moveHandler )
$resizeLeft . Add_MouseLeftButtonUp ( $releaseHandler )
$resizeRight . Tag = 'Right'
$resizeRight . Add_PreviewMouseLeftButtonDown ( $resizeHandler )
$resizeRight . Add_MouseMove ( $moveHandler )
$resizeRight . Add_MouseLeftButtonUp ( $releaseHandler )
$resizeTop . Tag = 'Top'
$resizeTop . Add_PreviewMouseLeftButtonDown ( $resizeHandler )
$resizeTop . Add_MouseMove ( $moveHandler )
$resizeTop . Add_MouseLeftButtonUp ( $releaseHandler )
$resizeBottom . Tag = 'Bottom'
$resizeBottom . Add_PreviewMouseLeftButtonDown ( $resizeHandler )
$resizeBottom . Add_MouseMove ( $moveHandler )
$resizeBottom . Add_MouseLeftButtonUp ( $releaseHandler )
$resizeTopLeft . Tag = 'TopLeft'
$resizeTopLeft . Add_PreviewMouseLeftButtonDown ( $resizeHandler )
$resizeTopLeft . Add_MouseMove ( $moveHandler )
$resizeTopLeft . Add_MouseLeftButtonUp ( $releaseHandler )
$resizeTopRight . Tag = 'TopRight'
$resizeTopRight . Add_PreviewMouseLeftButtonDown ( $resizeHandler )
$resizeTopRight . Add_MouseMove ( $moveHandler )
$resizeTopRight . Add_MouseLeftButtonUp ( $releaseHandler )
$resizeBottomLeft . Tag = 'BottomLeft'
$resizeBottomLeft . Add_PreviewMouseLeftButtonDown ( $resizeHandler )
$resizeBottomLeft . Add_MouseMove ( $moveHandler )
$resizeBottomLeft . Add_MouseLeftButtonUp ( $releaseHandler )
$resizeBottomRight . Tag = 'BottomRight'
$resizeBottomRight . Add_PreviewMouseLeftButtonDown ( $resizeHandler )
$resizeBottomRight . Add_MouseMove ( $moveHandler )
$resizeBottomRight . Add_MouseLeftButtonUp ( $releaseHandler )
2026-02-01 01:41:12 +01:00
# Integrated App Selection UI
$appsPanel = $window . FindName ( 'AppSelectionPanel' )
$onlyInstalledAppsBox = $window . FindName ( 'OnlyInstalledAppsBox' )
$loadingAppsIndicator = $window . FindName ( 'LoadingAppsIndicator' )
$appSelectionStatus = $window . FindName ( 'AppSelectionStatus' )
$defaultAppsBtn = $window . FindName ( 'DefaultAppsBtn' )
$clearAppSelectionBtn = $window . FindName ( 'ClearAppSelectionBtn' )
# Track the last selected checkbox for shift-click range selection
$script:MainWindowLastSelectedCheckbox = $null
# Track current app loading operation to prevent race conditions
$script:CurrentAppLoadTimer = $null
$script:CurrentAppLoadJob = $null
$script:CurrentAppLoadJobStartTime = $null
# Apply Tab UI Elements
$consoleOutput = $window . FindName ( 'ConsoleOutput' )
$consoleScrollViewer = $window . FindName ( 'ConsoleScrollViewer' )
$finishBtn = $window . FindName ( 'FinishBtn' )
$finishBtnText = $window . FindName ( 'FinishBtnText' )
# Set script-level variables for Write-ToConsole function
$script:GuiConsoleOutput = $consoleOutput
$script:GuiConsoleScrollViewer = $consoleScrollViewer
$script:GuiWindow = $window
2024-08-02 13:31:20 +02:00
2026-02-01 01:41:12 +01:00
# Updates app selection status text in the App Selection tab
function UpdateAppSelectionStatus {
$selectedCount = 0
foreach ( $child in $appsPanel . Children ) {
if ( $child -is [ System.Windows.Controls.CheckBox ] -and $child . IsChecked ) {
$selectedCount + +
}
}
$appSelectionStatus . Text = " $selectedCount app(s) selected for removal "
2020-10-27 23:26:39 +01:00
}
2023-10-15 19:48:27 +02:00
2026-02-01 01:41:12 +01:00
# Dynamically builds Tweaks UI from Features.json
function BuildDynamicTweaks {
$featuresJson = LoadJsonFile -filePath $script:FeaturesFilePath -expectedVersion " 1.0 "
2020-10-27 23:26:39 +01:00
2026-02-01 01:41:12 +01:00
if ( -not $featuresJson ) {
[ System.Windows.MessageBox ] :: Show ( " Unable to load Features.json file! " , " Error " , [ System.Windows.MessageBoxButton ] :: OK , [ System.Windows.MessageBoxImage ] :: Error ) | Out-Null
Exit
}
2023-08-03 22:24:27 +02:00
2026-02-01 01:41:12 +01:00
# Column containers
$col0 = $window . FindName ( 'Column0Panel' )
$col1 = $window . FindName ( 'Column1Panel' )
$col2 = $window . FindName ( 'Column2Panel' )
$columns = @ ( $col0 , $col1 , $col2 ) | Where-Object { $_ -ne $null }
2023-11-19 23:40:35 +01:00
2026-02-01 01:41:12 +01:00
# Clear all columns for fully dynamic panel creation
foreach ( $col in $columns ) {
if ( $col ) { $col . Children . Clear ( ) }
}
2024-03-04 12:56:12 +01:00
2026-02-01 01:41:12 +01:00
$script:UiControlMappings = @ { }
$script:CategoryCardMap = @ { }
2025-12-15 23:22:29 +01:00
2026-02-01 01:41:12 +01:00
function CreateLabeledCombo($parent , $labelText , $comboName , $items ) {
# If only 2 items (No Change + one option), use a checkbox instead
if ( $items . Count -eq 2 ) {
$checkbox = New-Object System . Windows . Controls . CheckBox
$checkbox . Content = $labelText
$checkbox . Name = $comboName
2026-02-02 21:42:29 +02:00
$checkbox . SetValue ( [ System.Windows.Automation.AutomationProperties ] :: NameProperty , $labelText )
2026-02-01 01:41:12 +01:00
$checkbox . IsChecked = $false
$checkbox . Style = $window . Resources [ " FeatureCheckboxStyle " ]
$parent . Children . Add ( $checkbox ) | Out-Null
# Register the checkbox with the window's name scope
try {
[ System.Windows.NameScope ] :: SetNameScope ( $checkbox , [ System.Windows.NameScope ] :: GetNameScope ( $window ) )
$window . RegisterName ( $comboName , $checkbox )
}
catch {
# Name might already be registered, ignore
}
return $checkbox
}
# Otherwise use a combobox for multiple options
2026-02-02 21:54:40 +01:00
# Wrap label in a Border for search highlighting
$lblBorder = New-Object System . Windows . Controls . Border
$lblBorder . Style = $window . Resources [ 'LabelBorderStyle' ]
$lblBorderName = " $comboName ` _LabelBorder "
$lblBorder . Name = $lblBorderName
2026-02-01 01:41:12 +01:00
$lbl = New-Object System . Windows . Controls . TextBlock
$lbl . Text = $labelText
$lbl . Style = $window . Resources [ 'LabelStyle' ]
$labelName = " $comboName ` _Label "
$lbl . Name = $labelName
2026-02-02 21:54:40 +01:00
$lblBorder . Child = $lbl
$parent . Children . Add ( $lblBorder ) | Out-Null
# Register the label border with the window's name scope
2026-02-01 01:41:12 +01:00
try {
2026-02-02 21:54:40 +01:00
[ System.Windows.NameScope ] :: SetNameScope ( $lblBorder , [ System.Windows.NameScope ] :: GetNameScope ( $window ) )
$window . RegisterName ( $lblBorderName , $lblBorder )
2026-02-01 01:41:12 +01:00
}
catch {
# Name might already be registered, ignore
}
2025-12-15 23:22:29 +01:00
2026-02-01 01:41:12 +01:00
$combo = New-Object System . Windows . Controls . ComboBox
$combo . Name = $comboName
2026-02-02 21:42:29 +02:00
$combo . SetValue ( [ System.Windows.Automation.AutomationProperties ] :: NameProperty , $labelText )
2026-02-01 01:41:12 +01:00
foreach ( $it in $items ) { $cbItem = New-Object System . Windows . Controls . ComboBoxItem ; $cbItem . Content = $it ; $combo . Items . Add ( $cbItem ) | Out-Null }
$combo . SelectedIndex = 0
$parent . Children . Add ( $combo ) | Out-Null
# Register the combo box with the window's name scope
try {
[ System.Windows.NameScope ] :: SetNameScope ( $combo , [ System.Windows.NameScope ] :: GetNameScope ( $window ) )
$window . RegisterName ( $comboName , $combo )
2025-12-15 23:22:29 +01:00
}
2026-02-01 01:41:12 +01:00
catch {
# Name might already be registered, ignore
2024-03-29 16:56:29 +01:00
}
2026-02-01 01:41:12 +01:00
return $combo
}
2024-07-02 18:51:00 +02:00
2026-02-01 01:41:12 +01:00
function GetOrCreateCategoryCard($category ) {
if ( -not $category ) { $category = 'Other' }
2025-12-26 20:36:51 +01:00
2026-02-01 01:41:12 +01:00
if ( $script:CategoryCardMap . ContainsKey ( $category ) ) { return $script:CategoryCardMap [ $category ] }
2025-12-15 23:22:29 +01:00
2026-02-01 01:41:12 +01:00
# Create a new card Border + StackPanel and add to shortest column
2026-02-04 13:47:47 +01:00
$target = $columns | Sort-Object @ { Expression = { $_ . Children . Count } ; Ascending = $true } , @ { Expression = { $columns . IndexOf ( $_ ) } ; Ascending = $true } | Select-Object -First 1
2023-11-19 23:40:35 +01:00
2026-02-01 01:41:12 +01:00
$border = New-Object System . Windows . Controls . Border
$border . Style = $window . Resources [ 'CategoryCardBorderStyle' ]
$border . Tag = 'DynamicCategory'
2024-11-01 18:01:40 +01:00
2026-02-01 01:41:12 +01:00
$panel = New-Object System . Windows . Controls . StackPanel
$safe = ( $category -replace '[^a-zA-Z0-9_]' , '_' )
$panel . Name = " Category_{0}_Panel " -f $safe
2023-11-19 23:40:35 +01:00
2026-02-01 01:41:12 +01:00
$header = New-Object System . Windows . Controls . TextBlock
$header . Text = $category
$header . Style = $window . Resources [ 'CategoryHeaderTextBlock' ]
$panel . Children . Add ( $header ) | Out-Null
$border . Child = $panel
$target . Children . Add ( $border ) | Out-Null
$script:CategoryCardMap [ $category ] = $panel
return $panel
2024-03-29 16:56:29 +01:00
}
2025-12-15 23:22:29 +01:00
2026-02-01 01:41:12 +01:00
# Determine categories present (from lists and features)
$categoriesPresent = @ { }
if ( $featuresJson . UiGroups ) {
foreach ( $g in $featuresJson . UiGroups ) { if ( $g . Category ) { $categoriesPresent [ $g . Category ] = $true } }
2025-12-15 23:22:29 +01:00
}
2026-02-01 01:41:12 +01:00
foreach ( $f in $featuresJson . Features ) { if ( $f . Category ) { $categoriesPresent [ $f . Category ] = $true } }
# Create cards in the order defined in Features.json Categories (if present)
$orderedCategories = @ ( )
if ( $featuresJson . Categories ) {
foreach ( $c in $featuresJson . Categories ) { if ( $categoriesPresent . ContainsKey ( $c ) ) { $orderedCategories + = $c } }
} else {
$orderedCategories = $categoriesPresent . Keys
2025-12-15 23:22:29 +01:00
}
2025-12-26 20:36:51 +01:00
2026-02-01 01:41:12 +01:00
foreach ( $category in $orderedCategories ) {
# Create/get card for this category
$panel = GetOrCreateCategoryCard -category $category
if ( -not $panel ) { continue }
2023-11-19 23:40:35 +01:00
2026-02-01 01:41:12 +01:00
# Collect groups and features for this category, then sort by priority
$categoryItems = @ ( )
2023-11-19 23:40:35 +01:00
2026-02-01 01:41:12 +01:00
# Add any groups for this category
if ( $featuresJson . UiGroups ) {
$groupIndex = 0
foreach ( $group in $featuresJson . UiGroups ) {
if ( $group . Category -ne $category ) { $groupIndex + + ; continue }
$categoryItems + = [ PSCustomObject ] @ {
Type = 'group'
Data = $group
Priority = if ( $null -ne $group . Priority ) { $group . Priority } else { [ int ] :: MaxValue }
OriginalIndex = $groupIndex
}
$groupIndex + +
}
}
2024-07-02 18:51:00 +02:00
2026-02-01 01:41:12 +01:00
# Add individual features for this category
$featureIndex = 0
foreach ( $feature in $featuresJson . Features ) {
if ( $feature . Category -ne $category ) { $featureIndex + + ; continue }
# Check version and feature compatibility using Features.json
if ( ( $feature . MinVersion -and $WinVersion -lt $feature . MinVersion ) -or ( $feature . MaxVersion -and $WinVersion -gt $feature . MaxVersion ) -or ( $feature . FeatureId -eq 'DisableModernStandbyNetworking' -and ( -not $script:ModernStandbySupported ) ) ) {
$featureIndex + + ; continue
}
2024-07-02 18:51:00 +02:00
2026-02-01 01:41:12 +01:00
# Skip if feature part of a group
$inGroup = $false
if ( $featuresJson . UiGroups ) {
foreach ( $g in $featuresJson . UiGroups ) { foreach ( $val in $g . Values ) { if ( $val . FeatureIds -contains $feature . FeatureId ) { $inGroup = $true ; break } } ; if ( $inGroup ) { break } }
}
if ( $inGroup ) { $featureIndex + + ; continue }
2024-07-02 18:51:00 +02:00
2026-02-01 01:41:12 +01:00
$categoryItems + = [ PSCustomObject ] @ {
Type = 'feature'
Data = $feature
Priority = if ( $null -ne $feature . Priority ) { $feature . Priority } else { [ int ] :: MaxValue }
OriginalIndex = $featureIndex
}
$featureIndex + +
}
2024-07-02 18:51:00 +02:00
2026-02-01 01:41:12 +01:00
# Sort by priority first, then by original index for items with same/no priority
$sortedItems = $categoryItems | Sort-Object -Property Priority , OriginalIndex
2024-07-02 18:51:00 +02:00
2026-02-01 01:41:12 +01:00
# Render sorted items
foreach ( $item in $sortedItems ) {
if ( $item . Type -eq 'group' ) {
$group = $item . Data
$items = @ ( 'No Change' ) + ( $group . Values | ForEach-Object { $_ . Label } )
$comboName = 'Group_{0}Combo' -f $group . GroupId
$combo = CreateLabeledCombo -parent $panel -labelText $group . Label -comboName $comboName -items $items
$script:UiControlMappings [ $comboName ] = @ { Type = 'group' ; Values = $group . Values ; Label = $group . Label }
}
elseif ( $item . Type -eq 'feature' ) {
$feature = $item . Data
$opt = 'Apply'
if ( $feature . FeatureId -match '^Disable' ) { $opt = 'Disable' } elseif ( $feature . FeatureId -match '^Enable' ) { $opt = 'Enable' }
$items = @ ( 'No Change' , $opt )
$comboName = ( " Feature_{0}_Combo " -f $feature . FeatureId ) -replace '[^a-zA-Z0-9_]' , ''
$combo = CreateLabeledCombo -parent $panel -labelText ( $feature . Action + ' ' + $feature . Label ) -comboName $comboName -items $items
$script:UiControlMappings [ $comboName ] = @ { Type = 'feature' ; FeatureId = $feature . FeatureId ; Action = $feature . Action }
}
2024-07-02 18:51:00 +02:00
}
}
}
2025-12-26 20:36:51 +01:00
2026-02-01 01:41:12 +01:00
# Helper function to complete app loading with the WinGet list
function script: LoadAppsWithList($listOfApps ) {
$appsToAdd = GetAppsFromJson -OnlyInstalled: $onlyInstalledAppsBox . IsChecked -InstalledList $listOfApps -InitialCheckedFromJson: $false
2024-07-02 18:51:00 +02:00
2026-02-01 01:41:12 +01:00
# Reset the last selected checkbox when loading a new list
$script:MainWindowLastSelectedCheckbox = $null
2024-07-02 18:51:00 +02:00
2026-02-01 01:41:12 +01:00
# Sort apps alphabetically and add to panel
$appsToAdd | Sort-Object -Property DisplayName | ForEach-Object {
$checkbox = New-Object System . Windows . Controls . CheckBox
$checkbox . Content = $_ . DisplayName
2026-02-02 21:42:29 +02:00
$checkbox . SetValue ( [ System.Windows.Automation.AutomationProperties ] :: NameProperty , $_ . DisplayName )
2026-02-01 01:41:12 +01:00
$checkbox . Tag = $_ . AppId
$checkbox . IsChecked = $_ . IsChecked
$checkbox . ToolTip = $_ . Description
$checkbox . Style = $window . Resources [ " AppsPanelCheckBoxStyle " ]
# Store metadata in checkbox for later use
Add-Member -InputObject $checkbox -MemberType NoteProperty -Name " SelectedByDefault " -Value $_ . SelectedByDefault
# Add event handler to update status
$checkbox . Add_Checked ( { UpdateAppSelectionStatus } )
$checkbox . Add_Unchecked ( { UpdateAppSelectionStatus } )
# Attach shift-click behavior for range selection
AttachShiftClickBehavior -checkbox $checkbox -appsPanel $appsPanel -lastSelectedCheckboxRef ( [ ref ] $script:MainWindowLastSelectedCheckbox ) -updateStatusCallback { UpdateAppSelectionStatus }
$appsPanel . Children . Add ( $checkbox ) | Out-Null
}
# Hide loading indicator and navigation blocker, update status
$loadingAppsIndicator . Visibility = 'Collapsed'
UpdateAppSelectionStatus
}
# Loads apps into the UI
function LoadAppsIntoMainUI {
# Cancel any existing load operation to prevent race conditions
if ( $script:CurrentAppLoadTimer -and $script:CurrentAppLoadTimer . IsEnabled ) {
$script:CurrentAppLoadTimer . Stop ( )
}
if ( $script:CurrentAppLoadJob ) {
Remove-Job -Job $script:CurrentAppLoadJob -Force -ErrorAction SilentlyContinue
}
$script:CurrentAppLoadTimer = $null
$script:CurrentAppLoadJob = $null
$script:CurrentAppLoadJobStartTime = $null
# Show loading indicator and navigation blocker, clear existing apps immediately
$loadingAppsIndicator . Visibility = 'Visible'
$appsPanel . Children . Clear ( )
# Update navigation buttons to disable Next/Previous
UpdateNavigationButtons
# Force UI to update and render all changes (loading indicator, blocker, disabled buttons)
$window . Dispatcher . Invoke ( [ System.Windows.Threading.DispatcherPriority ] :: Render , [ action ] { } )
# Schedule the actual loading work to run after UI has updated
$window . Dispatcher . BeginInvoke ( [ System.Windows.Threading.DispatcherPriority ] :: Background , [ action ] {
$listOfApps = " "
if ( $onlyInstalledAppsBox . IsChecked -and ( $script:WingetInstalled -eq $true ) ) {
# Start job to get list of installed apps via WinGet (async helper)
$asyncJob = GetInstalledAppsViaWinget -Async
$script:CurrentAppLoadJob = $asyncJob . Job
$script:CurrentAppLoadJobStartTime = $asyncJob . StartTime
# Create timer to poll job status without blocking UI
$script:CurrentAppLoadTimer = New-Object System . Windows . Threading . DispatcherTimer
$script:CurrentAppLoadTimer . Interval = [ TimeSpan ] :: FromMilliseconds ( 100 )
$script:CurrentAppLoadTimer . Add_Tick ( {
# Check if this timer was cancelled (another load started)
if ( -not $script:CurrentAppLoadJob -or -not $script:CurrentAppLoadTimer -or -not $script:CurrentAppLoadJobStartTime ) {
if ( $script:CurrentAppLoadTimer ) { $script:CurrentAppLoadTimer . Stop ( ) }
return
}
$elapsed = ( Get-Date ) - $script:CurrentAppLoadJobStartTime
# Check if job is complete or timed out (10 seconds)
if ( $script:CurrentAppLoadJob . State -eq 'Completed' ) {
$script:CurrentAppLoadTimer . Stop ( )
$listOfApps = Receive-Job -Job $script:CurrentAppLoadJob
Remove-Job -Job $script:CurrentAppLoadJob -ErrorAction SilentlyContinue
$script:CurrentAppLoadJob = $null
$script:CurrentAppLoadTimer = $null
$script:CurrentAppLoadJobStartTime = $null
# Continue with loading apps
LoadAppsWithList $listOfApps
}
elseif ( $elapsed . TotalSeconds -gt 10 -or $script:CurrentAppLoadJob . State -eq 'Failed' ) {
$script:CurrentAppLoadTimer . Stop ( )
Remove-Job -Job $script:CurrentAppLoadJob -Force -ErrorAction SilentlyContinue
$script:CurrentAppLoadJob = $null
$script:CurrentAppLoadTimer = $null
$script:CurrentAppLoadJobStartTime = $null
# Show error that the script was unable to get list of apps from WinGet
[ System.Windows.MessageBox ] :: Show ( 'Unable to load list of installed apps via WinGet.' , 'Error' , 'OK' , 'Error' ) | Out-Null
$onlyInstalledAppsBox . IsChecked = $false
# Continue with loading all apps (unchecked now)
LoadAppsWithList " "
}
} )
$script:CurrentAppLoadTimer . Start ( )
return # Exit here, timer will continue the work
}
# If checkbox is not checked or winget not installed, load all apps immediately
LoadAppsWithList $listOfApps
} ) | Out-Null
}
# Event handlers for app selection
$onlyInstalledAppsBox . Add_Checked ( {
LoadAppsIntoMainUI
} )
$onlyInstalledAppsBox . Add_Unchecked ( {
LoadAppsIntoMainUI
} )
# Quick selection buttons - only select apps actually in those categories
$defaultAppsBtn . Add_Click ( {
foreach ( $child in $appsPanel . Children ) {
if ( $child -is [ System.Windows.Controls.CheckBox ] ) {
if ( $child . SelectedByDefault -eq $true ) {
$child . IsChecked = $true
} else {
$child . IsChecked = $false
}
}
}
} )
2024-07-02 18:51:00 +02:00
2026-02-01 01:41:12 +01:00
$clearAppSelectionBtn . Add_Click ( {
foreach ( $child in $appsPanel . Children ) {
if ( $child -is [ System.Windows.Controls.CheckBox ] ) {
$child . IsChecked = $false
}
}
} )
2024-07-02 18:51:00 +02:00
2026-02-02 21:54:40 +01:00
# Shared search highlighting configuration
$script:SearchHighlightColor = [ System.Windows.Media.SolidColorBrush ] :: new ( [ System.Windows.Media.ColorConverter ] :: ConvertFromString ( " #FFF4CE " ) )
$script:SearchHighlightColorDark = [ System.Windows.Media.SolidColorBrush ] :: new ( [ System.Windows.Media.ColorConverter ] :: ConvertFromString ( " #4A4A2A " ) )
# Helper function to get the appropriate highlight brush based on theme
function GetSearchHighlightBrush {
if ( $usesDarkMode ) { return $script:SearchHighlightColorDark }
return $script:SearchHighlightColor
}
# Helper function to scroll to an item if it's not visible, centering it in the viewport
function ScrollToItemIfNotVisible {
param (
[ System.Windows.Controls.ScrollViewer ] $scrollViewer ,
[ System.Windows.UIElement ] $item ,
[ System.Windows.UIElement ] $container
)
if ( -not $scrollViewer -or -not $item -or -not $container ) { return }
try {
$itemPosition = $item . TransformToAncestor ( $container ) . Transform ( [ System.Windows.Point ] :: new ( 0 , 0 ) ) . Y
$viewportHeight = $scrollViewer . ViewportHeight
$itemHeight = $item . ActualHeight
$currentOffset = $scrollViewer . VerticalOffset
# Check if the item is currently visible in the viewport
$itemTop = $itemPosition - $currentOffset
$itemBottom = $itemTop + $itemHeight
$isVisible = ( $itemTop -ge 0 ) -and ( $itemBottom -le $viewportHeight )
# Only scroll if the item is not visible
if ( -not $isVisible ) {
# Center the item in the viewport
$targetOffset = $itemPosition - ( $viewportHeight / 2 ) + ( $itemHeight / 2 )
$scrollViewer . ScrollToVerticalOffset ( [ Math ] :: Max ( 0 , $targetOffset ) )
}
}
catch {
# Fallback to simple bring into view
$item . BringIntoView ( )
}
}
# Helper function to find the parent ScrollViewer of an element
function FindParentScrollViewer {
param ( [ System.Windows.UIElement ] $element )
$parent = [ System.Windows.Media.VisualTreeHelper ] :: GetParent ( $element )
while ( $null -ne $parent ) {
if ( $parent -is [ System.Windows.Controls.ScrollViewer ] ) {
return $parent
}
$parent = [ System.Windows.Media.VisualTreeHelper ] :: GetParent ( $parent )
}
return $null
}
2026-02-01 01:41:12 +01:00
# App Search Box functionality
$appSearchBox = $window . FindName ( 'AppSearchBox' )
$appSearchPlaceholder = $window . FindName ( 'AppSearchPlaceholder' )
$appSearchBox . Add_TextChanged ( {
$searchText = $appSearchBox . Text . ToLower ( ) . Trim ( )
# Show/hide placeholder
2026-02-02 21:54:40 +01:00
$appSearchPlaceholder . Visibility = if ( [ string ] :: IsNullOrWhiteSpace ( $appSearchBox . Text ) ) { 'Visible' } else { 'Collapsed' }
2026-02-01 01:41:12 +01:00
# Clear all highlights first
foreach ( $child in $appsPanel . Children ) {
if ( $child -is [ System.Windows.Controls.CheckBox ] ) {
$child . Background = [ System.Windows.Media.Brushes ] :: Transparent
}
}
2026-02-02 21:54:40 +01:00
if ( [ string ] :: IsNullOrWhiteSpace ( $searchText ) ) { return }
2026-02-01 01:41:12 +01:00
# Find and highlight all matching apps
$firstMatch = $null
2026-02-02 21:54:40 +01:00
$highlightBrush = GetSearchHighlightBrush
2026-02-01 01:41:12 +01:00
foreach ( $child in $appsPanel . Children ) {
2026-02-02 21:54:40 +01:00
if ( $child -is [ System.Windows.Controls.CheckBox ] -and $child . Visibility -eq 'Visible' ) {
if ( $child . Content . ToString ( ) . ToLower ( ) . Contains ( $searchText ) ) {
$child . Background = $highlightBrush
if ( $null -eq $firstMatch ) { $firstMatch = $child }
2026-02-01 01:41:12 +01:00
}
}
}
2026-02-02 21:54:40 +01:00
# Scroll to first match if not visible
2026-02-01 01:41:12 +01:00
if ( $firstMatch ) {
2026-02-02 21:54:40 +01:00
$scrollViewer = FindParentScrollViewer -element $appsPanel
if ( $scrollViewer ) {
ScrollToItemIfNotVisible -scrollViewer $scrollViewer -item $firstMatch -container $appsPanel
}
}
} )
# Tweak Search Box functionality
$tweakSearchBox = $window . FindName ( 'TweakSearchBox' )
$tweakSearchPlaceholder = $window . FindName ( 'TweakSearchPlaceholder' )
2026-02-04 11:34:55 +01:00
$tweakSearchBorder = $window . FindName ( 'TweakSearchBorder' )
2026-02-02 21:54:40 +01:00
$tweaksScrollViewer = $window . FindName ( 'TweaksScrollViewer' )
$tweaksGrid = $window . FindName ( 'TweaksGrid' )
$col0 = $window . FindName ( 'Column0Panel' )
$col1 = $window . FindName ( 'Column1Panel' )
$col2 = $window . FindName ( 'Column2Panel' )
2026-02-04 11:34:55 +01:00
# Monitor scrollbar visibility and adjust searchbar margin
$tweaksScrollViewer . Add_ScrollChanged ( {
if ( $tweaksScrollViewer . ScrollableHeight -gt 0 ) {
# The 17px accounts for the scrollbar width + some padding
$tweakSearchBorder . Margin = [ System.Windows.Thickness ] :: new ( 0 , 0 , 17 , 0 )
} else {
$tweakSearchBorder . Margin = [ System.Windows.Thickness ] :: new ( 0 , 0 , 0 , 0 )
}
} )
2026-02-02 21:54:40 +01:00
# Helper function to clear all tweak highlights
function ClearTweakHighlights {
$columns = @ ( $col0 , $col1 , $col2 ) | Where-Object { $_ -ne $null }
foreach ( $column in $columns ) {
foreach ( $card in $column . Children ) {
if ( $card -is [ System.Windows.Controls.Border ] -and $card . Child -is [ System.Windows.Controls.StackPanel ] ) {
foreach ( $control in $card . Child . Children ) {
if ( $control -is [ System.Windows.Controls.CheckBox ] -or
( $control -is [ System.Windows.Controls.Border ] -and $control . Name -like '*_LabelBorder' ) ) {
$control . Background = [ System.Windows.Media.Brushes ] :: Transparent
}
}
2026-02-01 01:41:12 +01:00
}
}
2026-02-02 21:54:40 +01:00
}
}
# Helper function to check if a ComboBox contains matching items
function ComboBoxContainsMatch {
param ( [ System.Windows.Controls.ComboBox ] $comboBox , [ string ] $searchText )
foreach ( $item in $comboBox . Items ) {
$itemText = if ( $item -is [ System.Windows.Controls.ComboBoxItem ] ) { $item . Content . ToString ( ) . ToLower ( ) } else { $item . ToString ( ) . ToLower ( ) }
if ( $itemText . Contains ( $searchText ) ) { return $true }
}
return $false
}
$tweakSearchBox . Add_TextChanged ( {
$searchText = $tweakSearchBox . Text . ToLower ( ) . Trim ( )
# Show/hide placeholder
$tweakSearchPlaceholder . Visibility = if ( [ string ] :: IsNullOrWhiteSpace ( $tweakSearchBox . Text ) ) { 'Visible' } else { 'Collapsed' }
# Clear all highlights
ClearTweakHighlights
if ( [ string ] :: IsNullOrWhiteSpace ( $searchText ) ) { return }
# Find and highlight all matching tweaks
$firstMatch = $null
$highlightBrush = GetSearchHighlightBrush
$columns = @ ( $col0 , $col1 , $col2 ) | Where-Object { $_ -ne $null }
foreach ( $column in $columns ) {
foreach ( $card in $column . Children ) {
if ( $card -is [ System.Windows.Controls.Border ] -and $card . Child -is [ System.Windows.Controls.StackPanel ] ) {
$controlsList = @ ( $card . Child . Children )
for ( $i = 0 ; $i -lt $controlsList . Count ; $i + + ) {
$control = $controlsList [ $i ]
$matchFound = $false
$controlToHighlight = $null
if ( $control -is [ System.Windows.Controls.CheckBox ] ) {
if ( $control . Content . ToString ( ) . ToLower ( ) . Contains ( $searchText ) ) {
$matchFound = $true
$controlToHighlight = $control
}
}
elseif ( $control -is [ System.Windows.Controls.Border ] -and $control . Name -like '*_LabelBorder' ) {
$labelText = if ( $control . Child ) { $control . Child . Text . ToLower ( ) } else { " " }
$comboBox = if ( $i + 1 -lt $controlsList . Count -and $controlsList [ $i + 1 ] -is [ System.Windows.Controls.ComboBox ] ) { $controlsList [ $i + 1 ] } else { $null }
# Check label text or combo box items
if ( $labelText . Contains ( $searchText ) -or ( $comboBox -and ( ComboBoxContainsMatch -comboBox $comboBox -searchText $searchText ) ) ) {
$matchFound = $true
$controlToHighlight = $control
}
}
if ( $matchFound -and $controlToHighlight ) {
$controlToHighlight . Background = $highlightBrush
if ( $null -eq $firstMatch ) { $firstMatch = $controlToHighlight }
}
}
}
2026-02-01 01:41:12 +01:00
}
}
2026-02-02 21:54:40 +01:00
# Scroll to first match if not visible
if ( $firstMatch -and $tweaksScrollViewer ) {
ScrollToItemIfNotVisible -scrollViewer $tweaksScrollViewer -item $firstMatch -container $tweaksGrid
}
2026-02-01 01:41:12 +01:00
} )
2026-02-04 11:43:36 +01:00
# Add Ctrl+F keyboard shortcut to focus search box on current tab
$window . Add_KeyDown ( {
param ( $sender , $e )
# Check if Ctrl+F was pressed
if ( $e . Key -eq [ System.Windows.Input.Key ] :: F -and
( [ System.Windows.Input.Keyboard ] :: Modifiers -band [ System.Windows.Input.ModifierKeys ] :: Control ) ) {
$currentTab = $tabControl . SelectedItem
# Focus AppSearchBox if on App Removal tab
if ( $currentTab . Header -eq " App Removal " -and $appSearchBox ) {
$appSearchBox . Focus ( )
$e . Handled = $true
}
# Focus TweakSearchBox if on Tweaks tab
elseif ( $currentTab . Header -eq " Tweaks " -and $tweakSearchBox ) {
$tweakSearchBox . Focus ( )
$e . Handled = $true
}
}
} )
2026-02-01 01:41:12 +01:00
# Wizard Navigation
$tabControl = $window . FindName ( 'MainTabControl' )
$previousBtn = $window . FindName ( 'PreviousBtn' )
$nextBtn = $window . FindName ( 'NextBtn' )
$userSelectionCombo = $window . FindName ( 'UserSelectionCombo' )
$userSelectionDescription = $window . FindName ( 'UserSelectionDescription' )
$otherUserPanel = $window . FindName ( 'OtherUserPanel' )
$otherUsernameTextBox = $window . FindName ( 'OtherUsernameTextBox' )
$usernameTextBoxPlaceholder = $window . FindName ( 'UsernameTextBoxPlaceholder' )
$usernameValidationMessage = $window . FindName ( 'UsernameValidationMessage' )
# Navigation button handlers
function UpdateNavigationButtons {
$currentIndex = $tabControl . SelectedIndex
$totalTabs = $tabControl . Items . Count
$homeIndex = 0
$overviewIndex = $totalTabs - 2
$applyIndex = $totalTabs - 1
# Navigation button visibility
if ( $currentIndex -eq $homeIndex ) {
$nextBtn . Visibility = 'Collapsed'
$previousBtn . Visibility = 'Collapsed'
} elseif ( $currentIndex -eq $overviewIndex ) {
$nextBtn . Visibility = 'Collapsed'
$previousBtn . Visibility = 'Visible'
} elseif ( $currentIndex -eq $applyIndex ) {
$nextBtn . Visibility = 'Collapsed'
$previousBtn . Visibility = 'Collapsed'
} else {
$nextBtn . Visibility = 'Visible'
$previousBtn . Visibility = 'Visible'
}
# Update progress indicators
# Tab indices: 0=Home, 1=App Removal, 2=Tweaks, 3=Overview, 4=Apply
$blueColor = " #0067c0 "
$greyColor = " #808080 "
$progressIndicator1 = $window . FindName ( 'ProgressIndicator1' ) # App Removal
$progressIndicator2 = $window . FindName ( 'ProgressIndicator2' ) # Tweaks
$progressIndicator3 = $window . FindName ( 'ProgressIndicator3' ) # Overview
$bottomNavGrid = $window . FindName ( 'BottomNavGrid' )
# Hide bottom navigation on home page and apply tab
if ( $currentIndex -eq 0 -or $currentIndex -eq $applyIndex ) {
$bottomNavGrid . Visibility = 'Collapsed'
} else {
$bottomNavGrid . Visibility = 'Visible'
}
# Update indicator colors based on current tab
# Indicator 1 (App Removal) - tab index 1
if ( $currentIndex -ge 1 ) {
$progressIndicator1 . Fill = $blueColor
} else {
$progressIndicator1 . Fill = $greyColor
}
# Indicator 2 (Tweaks) - tab index 2
if ( $currentIndex -ge 2 ) {
$progressIndicator2 . Fill = $blueColor
} else {
$progressIndicator2 . Fill = $greyColor
}
# Indicator 3 (Overview) - tab index 3
if ( $currentIndex -ge 3 ) {
$progressIndicator3 . Fill = $blueColor
} else {
$progressIndicator3 . Fill = $greyColor
}
}
# Update user selection description and show/hide other user panel
$userSelectionCombo . Add_SelectionChanged ( {
switch ( $userSelectionCombo . SelectedIndex ) {
0 {
$userSelectionDescription . Text = " Changes will be applied to the currently logged-in user profile. "
$otherUserPanel . Visibility = 'Collapsed'
$usernameValidationMessage . Text = " "
}
1 {
$userSelectionDescription . Text = " Changes will be applied to a different user profile on this system. "
$otherUserPanel . Visibility = 'Visible'
$usernameValidationMessage . Text = " "
}
2 {
$userSelectionDescription . Text = " Changes will be applied to the default user template, affecting all new users created after this point. Useful for Sysprep deployment. "
$otherUserPanel . Visibility = 'Collapsed'
$usernameValidationMessage . Text = " "
}
}
} )
$otherUsernameTextBox . Add_TextChanged ( {
# Show/hide placeholder
if ( [ string ] :: IsNullOrWhiteSpace ( $otherUsernameTextBox . Text ) ) {
$usernameTextBoxPlaceholder . Visibility = 'Visible'
} else {
$usernameTextBoxPlaceholder . Visibility = 'Collapsed'
}
ValidateOtherUsername
} )
function ValidateOtherUsername {
# Only validate if "Other User" is selected
if ( $userSelectionCombo . SelectedIndex -ne 1 ) {
return $true
}
$username = $otherUsernameTextBox . Text . Trim ( )
$errorBrush = [ System.Windows.Media.SolidColorBrush ] :: new ( [ System.Windows.Media.ColorConverter ] :: ConvertFromString ( " #c42b1c " ) )
$successBrush = [ System.Windows.Media.SolidColorBrush ] :: new ( [ System.Windows.Media.ColorConverter ] :: ConvertFromString ( " #28a745 " ) )
if ( $username . Length -eq 0 ) {
$usernameValidationMessage . Text = " [X] Please enter a username "
$usernameValidationMessage . Foreground = $errorBrush
return $false
}
if ( $username -eq $env:USERNAME ) {
$usernameValidationMessage . Text = " [X] Cannot enter your own username, use 'Current User' option instead "
$usernameValidationMessage . Foreground = $errorBrush
return $false
}
$userExists = CheckIfUserExists -Username $username
if ( $userExists ) {
$usernameValidationMessage . Text = " [OK] User found: $username "
$usernameValidationMessage . Foreground = $successBrush
return $true
}
$usernameValidationMessage . Text = " [X] User not found, please enter a valid username "
$usernameValidationMessage . Foreground = $errorBrush
return $false
}
function GenerateOverview {
# Load Features.json
$featuresJson = LoadJsonFile -filePath $script:FeaturesFilePath -expectedVersion " 1.0 "
$overviewChangesPanel = $window . FindName ( 'OverviewChangesPanel' )
$overviewChangesPanel . Children . Clear ( )
$changesList = @ ( )
# Collect selected apps
$selectedAppsCount = 0
foreach ( $child in $appsPanel . Children ) {
if ( $child -is [ System.Windows.Controls.CheckBox ] -and $child . IsChecked ) {
$selectedAppsCount + +
}
}
if ( $selectedAppsCount -gt 0 ) {
$changesList + = " Remove $selectedAppsCount selected application(s) "
}
# Collect all ComboBox/CheckBox selections from dynamically created controls
if ( $script:UiControlMappings ) {
foreach ( $mappingKey in $script:UiControlMappings . Keys ) {
$control = $window . FindName ( $mappingKey )
$isSelected = $false
# Check if it's a checkbox or combobox
if ( $control -is [ System.Windows.Controls.CheckBox ] ) {
$isSelected = $control . IsChecked -eq $true
}
elseif ( $control -is [ System.Windows.Controls.ComboBox ] ) {
$isSelected = $control . SelectedIndex -gt 0
}
if ( $control -and $isSelected ) {
$mapping = $script:UiControlMappings [ $mappingKey ]
if ( $mapping . Type -eq 'group' ) {
# For combobox: SelectedIndex 0 = No Change, so subtract 1 to index into Values
$selectedValue = $mapping . Values [ $control . SelectedIndex - 1 ]
foreach ( $fid in $selectedValue . FeatureIds ) {
$feature = $featuresJson . Features | Where-Object { $_ . FeatureId -eq $fid }
if ( $feature ) { $changesList + = ( $feature . Action + ' ' + $feature . Label ) }
}
}
elseif ( $mapping . Type -eq 'feature' ) {
$feature = $featuresJson . Features | Where-Object { $_ . FeatureId -eq $mapping . FeatureId }
if ( $feature ) { $changesList + = ( $feature . Action + ' ' + $feature . Label ) }
}
}
}
}
if ( $changesList . Count -eq 0 ) {
$textBlock = New-Object System . Windows . Controls . TextBlock
$textBlock . Text = " No changes selected "
$textBlock . Style = $window . Resources [ " OverviewNoChangesTextStyle " ]
$overviewChangesPanel . Children . Add ( $textBlock ) | Out-Null
2025-12-15 23:22:29 +01:00
}
else {
2026-02-01 01:41:12 +01:00
foreach ( $change in $changesList ) {
$bullet = New-Object System . Windows . Controls . TextBlock
$bullet . Text = " - $change "
$bullet . Style = $window . Resources [ " OverviewChangeBulletStyle " ]
$overviewChangesPanel . Children . Add ( $bullet ) | Out-Null
2024-07-02 18:51:00 +02:00
}
}
}
2026-02-01 01:41:12 +01:00
$previousBtn . Add_Click ( {
if ( $tabControl . SelectedIndex -gt 0 ) {
$tabControl . SelectedIndex - -
UpdateNavigationButtons
}
} )
2024-07-02 18:51:00 +02:00
2026-02-01 01:41:12 +01:00
$nextBtn . Add_Click ( {
if ( $tabControl . SelectedIndex -lt ( $tabControl . Items . Count - 1 ) ) {
$tabControl . SelectedIndex + +
UpdateNavigationButtons
}
} )
# Handle Home Start button
$homeStartBtn = $window . FindName ( 'HomeStartBtn' )
$homeStartBtn . Add_Click ( {
# Navigate to first tab after home (App Removal)
$tabControl . SelectedIndex = 1
UpdateNavigationButtons
} )
# Handle Overview Apply Changes button - validates and immediately starts applying changes
$overviewApplyBtn = $window . FindName ( 'OverviewApplyBtn' )
$overviewApplyBtn . Add_Click ( {
if ( -not ( ValidateOtherUsername ) ) {
[ System.Windows.MessageBox ] :: Show ( " Please enter a valid username. " , " Invalid Username " , [ System.Windows.MessageBoxButton ] :: OK , [ System.Windows.MessageBoxImage ] :: Warning ) | Out-Null
return
}
2025-08-22 17:50:47 +02:00
2026-02-01 01:41:12 +01:00
$controlParamsCount = 0
foreach ( $Param in $script:ControlParams ) {
if ( $script:Params . ContainsKey ( $Param ) ) {
$controlParamsCount + +
2025-08-22 17:50:47 +02:00
}
2026-02-01 01:41:12 +01:00
}
2025-08-22 17:50:47 +02:00
2026-02-01 01:41:12 +01:00
# App Removal - collect selected apps from integrated UI
$selectedApps = @ ( )
foreach ( $child in $appsPanel . Children ) {
if ( $child -is [ System.Windows.Controls.CheckBox ] -and $child . IsChecked ) {
$selectedApps + = $child . Tag
2025-08-22 17:50:47 +02:00
}
}
2026-02-01 01:41:12 +01:00
if ( $selectedApps . Count -gt 0 ) {
# Check if Microsoft Store is selected
if ( $selectedApps -contains " Microsoft.WindowsStore " ) {
$result = [ System.Windows.MessageBox ] :: Show (
'Are you sure you wish to uninstall the Microsoft Store? This app cannot easily be reinstalled.' ,
'Are you sure?' ,
[ System.Windows.MessageBoxButton ] :: YesNo ,
[ System.Windows.MessageBoxImage ] :: Warning
)
2025-08-22 17:50:47 +02:00
2026-02-01 01:41:12 +01:00
if ( $result -eq [ System.Windows.MessageBoxResult ] :: No ) {
return
}
}
AddParameter 'RemoveApps'
AddParameter 'Apps' ( $selectedApps -join ',' )
}
# Apply dynamic tweaks selections
if ( $script:UiControlMappings ) {
foreach ( $mappingKey in $script:UiControlMappings . Keys ) {
$control = $window . FindName ( $mappingKey )
$isSelected = $false
$selectedIndex = 0
# Check if it's a checkbox or combobox
if ( $control -is [ System.Windows.Controls.CheckBox ] ) {
$isSelected = $control . IsChecked -eq $true
$selectedIndex = if ( $isSelected ) { 1 } else { 0 }
}
elseif ( $control -is [ System.Windows.Controls.ComboBox ] ) {
$isSelected = $control . SelectedIndex -gt 0
$selectedIndex = $control . SelectedIndex
}
if ( $control -and $isSelected ) {
$mapping = $script:UiControlMappings [ $mappingKey ]
if ( $mapping . Type -eq 'group' ) {
if ( $selectedIndex -gt 0 -and $selectedIndex -le $mapping . Values . Count ) {
$selectedValue = $mapping . Values [ $selectedIndex - 1 ]
foreach ( $fid in $selectedValue . FeatureIds ) {
AddParameter $fid
}
}
}
elseif ( $mapping . Type -eq 'feature' ) {
AddParameter $mapping . FeatureId
}
}
}
}
2025-08-22 17:50:47 +02:00
2026-02-01 01:41:12 +01:00
# Check if any changes were selected
$totalChanges = $script:Params . Count - $controlParamsCount
2025-08-22 17:50:47 +02:00
2026-02-01 01:41:12 +01:00
# Apps parameter does not count as a change itself
if ( $script:Params . ContainsKey ( 'Apps' ) ) {
$totalChanges - -
}
2025-09-07 15:04:29 +02:00
2026-02-01 01:41:12 +01:00
if ( $totalChanges -eq 0 ) {
[ System.Windows.MessageBox ] :: Show (
'No changes have been selected, please select at least one item to proceed.' ,
'No Changes Selected' ,
[ System.Windows.MessageBoxButton ] :: OK ,
[ System.Windows.MessageBoxImage ] :: Information
)
return
}
2025-12-26 20:36:51 +01:00
2026-02-01 01:41:12 +01:00
# Check RestorePointCheckBox
$restorePointCheckBox = $window . FindName ( 'RestorePointCheckBox' )
if ( $restorePointCheckBox -and $restorePointCheckBox . IsChecked ) {
AddParameter 'CreateRestorePoint'
}
# Store selected user mode
switch ( $userSelectionCombo . SelectedIndex ) {
1 { AddParameter User ( $otherUsernameTextBox . Text . Trim ( ) ) }
2 { AddParameter Sysprep }
2025-09-28 20:40:24 +02:00
}
2025-12-26 20:36:51 +01:00
2026-02-01 01:41:12 +01:00
SaveSettings
2025-12-26 20:36:51 +01:00
2026-02-01 01:41:12 +01:00
# Navigate to Apply tab (last tab) and start applying changes
$tabControl . SelectedIndex = $tabControl . Items . Count - 1
# Clear console and set initial status
$consoleOutput . Text = " "
Write-ToConsole " Applying changes to $( if ( $script:Params . ContainsKey ( " Sysprep " ) ) { " default user template " } else { " user $( GetUserName ) " } ) "
Write-ToConsole " Total changes to apply: $totalChanges "
Write-ToConsole " "
# Run changes in background to keep UI responsive
$window . Dispatcher . BeginInvoke ( [ System.Windows.Threading.DispatcherPriority ] :: Background , [ action ] {
try {
ExecuteAllChanges
2026-02-06 23:44:39 +01:00
# Check if user wants to restart explorer (from checkbox)
$restartExplorerCheckBox = $window . FindName ( 'RestartExplorerCheckBox' )
if ( $restartExplorerCheckBox -and $restartExplorerCheckBox . IsChecked -and -not $script:CancelRequested ) {
RestartExplorer
2026-02-01 01:41:12 +01:00
}
Write-ToConsole " "
2026-02-06 23:44:39 +01:00
if ( $script:CancelRequested ) {
Write-ToConsole " Script execution was cancelled by the user. Some changes may not have been applied. "
} else {
Write-ToConsole " All changes have been applied. Please check the output above for any errors. "
}
2026-02-01 01:41:12 +01:00
$finishBtn . Dispatcher . Invoke ( [ action ] {
$finishBtn . IsEnabled = $true
$finishBtnText . Text = " Close Win11Debloat "
} )
}
catch {
Write-ToConsole " Error: $( $_ . Exception . Message ) "
$finishBtn . Dispatcher . Invoke ( [ action ] {
$finishBtn . IsEnabled = $true
$finishBtnText . Text = " Close Win11Debloat "
} )
}
} )
} )
2025-09-07 15:04:29 +02:00
2026-02-01 01:41:12 +01:00
# Initialize UI elements on window load
$window . Add_Loaded ( {
BuildDynamicTweaks
2025-09-07 15:04:29 +02:00
2026-02-01 01:41:12 +01:00
LoadAppsIntoMainUI
2025-09-07 15:04:29 +02:00
2026-02-06 23:44:39 +01:00
# Update Current User label with username
if ( $userSelectionCombo -and $userSelectionCombo . Items . Count -gt 0 ) {
$currentUserItem = $userSelectionCombo . Items [ 0 ]
if ( $currentUserItem -is [ System.Windows.Controls.ComboBoxItem ] ) {
$currentUserItem . Content = " Current User ( $( GetUserName ) ) "
}
}
# Disable Restart Explorer option if NoRestartExplorer parameter is set
$restartExplorerCheckBox = $window . FindName ( 'RestartExplorerCheckBox' )
if ( $restartExplorerCheckBox -and $script:Params . ContainsKey ( " NoRestartExplorer " ) ) {
$restartExplorerCheckBox . IsChecked = $false
$restartExplorerCheckBox . IsEnabled = $false
}
# Force Apply Changes To setting if Sysprep or User parameters are set
if ( $script:Params . ContainsKey ( " Sysprep " ) ) {
$userSelectionCombo . SelectedIndex = 2
$userSelectionCombo . IsEnabled = $false
}
elseif ( $script:Params . ContainsKey ( " User " ) ) {
$userSelectionCombo . SelectedIndex = 1
$userSelectionCombo . IsEnabled = $false
$otherUsernameTextBox . Text = $script:Params . Item ( " User " )
$otherUsernameTextBox . IsEnabled = $false
}
2026-02-01 01:41:12 +01:00
UpdateNavigationButtons
} )
2020-11-04 01:48:42 +01:00
2026-02-01 01:41:12 +01:00
# Add event handler for tab changes
$tabControl . Add_SelectionChanged ( {
# Regenerate overview when switching to Overview tab
if ( $tabControl . SelectedIndex -eq ( $tabControl . Items . Count - 2 ) ) {
GenerateOverview
}
UpdateNavigationButtons
} )
2024-06-27 23:08:43 +02:00
2026-02-01 01:41:12 +01:00
# Handle Load Defaults button
$loadDefaultsBtn = $window . FindName ( 'LoadDefaultsBtn' )
$loadDefaultsBtn . Add_Click ( {
$defaultsJson = LoadJsonFile -filePath $script:DefaultSettingsFilePath -expectedVersion " 1.0 "
2025-12-26 20:36:51 +01:00
2026-02-01 01:41:12 +01:00
if ( -not $defaultsJson ) {
[ System.Windows.MessageBox ] :: Show ( " Failed to load default settings file " , " Error " , [ System.Windows.MessageBoxButton ] :: OK , [ System.Windows.MessageBoxImage ] :: Error )
return
}
ApplySettingsToUiControls -window $window -settingsJson $defaultsJson -uiControlMappings $script:UiControlMappings
} )
2025-12-26 20:36:51 +01:00
2026-02-01 01:41:12 +01:00
# Handle Load Last Used settings and Load Last Used apps
$loadLastUsedBtn = $window . FindName ( 'LoadLastUsedBtn' )
$loadLastUsedAppsBtn = $window . FindName ( 'LoadLastUsedAppsBtn' )
2025-12-26 20:36:51 +01:00
2026-02-01 01:41:12 +01:00
$lastUsedSettingsJson = LoadJsonFile -filePath $script:SavedSettingsFilePath -expectedVersion " 1.0 " -optionalFile
$hasSettings = $false
$appsSetting = $null
if ( $lastUsedSettingsJson -and $lastUsedSettingsJson . Settings ) {
foreach ( $s in $lastUsedSettingsJson . Settings ) {
# Only count as hasSettings if a setting other than RemoveApps/Apps is present and true
if ( $s . Value -eq $true -and $s . Name -ne 'RemoveApps' -and $s . Name -ne 'Apps' ) { $hasSettings = $true }
if ( $s . Name -eq 'Apps' -and $s . Value ) { $appsSetting = $s . Value }
}
}
# Show option to load last used settings if they exist
if ( $hasSettings ) {
$loadLastUsedBtn . Add_Click ( {
try {
ApplySettingsToUiControls -window $window -settingsJson $lastUsedSettingsJson -uiControlMappings $script:UiControlMappings
}
catch {
[ System.Windows.MessageBox ] :: Show ( " Failed to load last used settings: $_ " , " Error " , [ System.Windows.MessageBoxButton ] :: OK , [ System.Windows.MessageBoxImage ] :: Error )
}
} )
2025-03-05 23:39:29 +01:00
}
else {
2026-02-01 01:41:12 +01:00
$loadLastUsedBtn . Visibility = 'Collapsed'
2024-06-27 23:08:43 +02:00
}
2026-02-01 01:41:12 +01:00
# Show option to load last used apps if they exist
if ( $appsSetting -and $appsSetting . ToString ( ) . Trim ( ) . Length -gt 0 ) {
$loadLastUsedAppsBtn . Add_Click ( {
try {
$savedApps = @ ( )
if ( $appsSetting -is [ string ] ) { $savedApps = $appsSetting . Split ( ',' ) }
elseif ( $appsSetting -is [ array ] ) { $savedApps = $appsSetting }
$savedApps = $savedApps | ForEach-Object { $_ . Trim ( ) } | Where-Object { $_ -ne '' }
foreach ( $child in $appsPanel . Children ) {
if ( $child -is [ System.Windows.Controls.CheckBox ] ) {
if ( $savedApps -contains $child . Tag ) { $child . IsChecked = $true } else { $child . IsChecked = $false }
}
}
}
catch {
[ System.Windows.MessageBox ] :: Show ( " Failed to load last used app selection: $_ " , " Error " , [ System.Windows.MessageBoxButton ] :: OK , [ System.Windows.MessageBoxImage ] :: Error )
}
} )
}
else {
$loadLastUsedAppsBtn . Visibility = 'Collapsed'
}
# Clear All Tweaks button
$clearAllTweaksBtn = $window . FindName ( 'ClearAllTweaksBtn' )
$clearAllTweaksBtn . Add_Click ( {
# Reset all ComboBoxes to index 0 (No Change) and uncheck all CheckBoxes
if ( $script:UiControlMappings ) {
foreach ( $comboName in $script:UiControlMappings . Keys ) {
$control = $window . FindName ( $comboName )
if ( $control -is [ System.Windows.Controls.CheckBox ] ) {
$control . IsChecked = $false
}
elseif ( $control -is [ System.Windows.Controls.ComboBox ] ) {
$control . SelectedIndex = 0
}
}
}
2023-08-03 22:24:27 +02:00
2026-02-01 01:41:12 +01:00
# Also uncheck RestorePointCheckBox
$restorePointCheckBox = $window . FindName ( 'RestorePointCheckBox' )
if ( $restorePointCheckBox ) {
$restorePointCheckBox . IsChecked = $false
}
} )
2025-12-26 20:36:51 +01:00
2026-02-01 01:41:12 +01:00
# Finish (Close Win11Debloat) button handler
$finishBtn . Add_Click ( {
$window . Close ( )
} )
2025-02-12 13:07:44 +01:00
2026-02-01 01:41:12 +01:00
# Show the window
return $window . ShowDialog ( )
}
2025-04-16 12:56:37 +02:00
2025-02-13 21:08:47 +01:00
2026-02-01 01:41:12 +01:00
# Shows application selection window that allows the user to select what apps they want to remove or keep
function OpenAppSelectionWindow {
Add-Type -AssemblyName PresentationFramework , PresentationCore , WindowsBase | Out-Null
2025-06-12 21:50:57 +02:00
2026-02-01 01:41:12 +01:00
$usesDarkMode = GetSystemUsesDarkMode
# Load XAML from file
$xaml = Get-Content -Path $script:AppSelectionSchema -Raw
$reader = [ System.Xml.XmlReader ] :: Create ( [ System.IO.StringReader ] :: new ( $xaml ) )
try {
$window = [ System.Windows.Markup.XamlReader ] :: Load ( $reader )
2024-06-05 19:18:01 +02:00
}
2026-02-01 01:41:12 +01:00
finally {
$reader . Close ( )
2024-06-05 19:18:01 +02:00
}
2023-09-08 00:47:59 +02:00
2026-02-01 01:41:12 +01:00
SetWindowThemeResources -window $window -usesDarkMode $usesDarkMode
2023-09-08 00:47:59 +02:00
2026-02-01 01:41:12 +01:00
$appsPanel = $window . FindName ( 'AppsPanel' )
$checkAllBox = $window . FindName ( 'CheckAllBox' )
$onlyInstalledBox = $window . FindName ( 'OnlyInstalledBox' )
$confirmBtn = $window . FindName ( 'ConfirmBtn' )
$loadingIndicator = $window . FindName ( 'LoadingAppsIndicator' )
$titleBar = $window . FindName ( 'TitleBar' )
# Track the last selected checkbox for shift-click range selection
$script:AppSelectionWindowLastSelectedCheckbox = $null
2023-09-08 00:47:59 +02:00
2026-02-01 01:41:12 +01:00
# Loads apps into the apps UI
function LoadApps {
# Show loading indicator
$loadingIndicator . Visibility = 'Visible'
$window . Dispatcher . Invoke ( [ System.Windows.Threading.DispatcherPriority ] :: Background , [ action ] { } )
2023-09-08 00:47:59 +02:00
2026-02-01 01:41:12 +01:00
$appsPanel . Children . Clear ( )
$listOfApps = " "
2024-05-14 23:56:58 +02:00
2026-02-01 01:41:12 +01:00
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 ) {
# Show error that the script was unable to get list of apps from WinGet
[ System.Windows.MessageBox ] :: Show ( 'Unable to load list of installed apps via WinGet.' , 'Error' , 'OK' , 'Error' ) | Out-Null
$onlyInstalledBox . IsChecked = $false
}
}
2023-09-08 00:47:59 +02:00
2026-02-01 01:41:12 +01:00
$appsToAdd = GetAppsFromJson -OnlyInstalled: $onlyInstalledBox . IsChecked -InstalledList $listOfApps -InitialCheckedFromJson
2024-06-27 23:08:43 +02:00
2026-02-01 01:41:12 +01:00
# Reset the last selected checkbox when loading a new list
$script:AppSelectionWindowLastSelectedCheckbox = $null
2024-06-27 23:08:43 +02:00
2026-02-01 01:41:12 +01:00
# Sort apps alphabetically and add to panel
$appsToAdd | Sort-Object -Property DisplayName | ForEach-Object {
$checkbox = New-Object System . Windows . Controls . CheckBox
$checkbox . Content = $_ . DisplayName
2026-02-02 21:42:29 +02:00
$checkbox . SetValue ( [ System.Windows.Automation.AutomationProperties ] :: NameProperty , $_ . DisplayName )
2026-02-01 01:41:12 +01:00
$checkbox . Tag = $_ . AppId
$checkbox . IsChecked = $_ . IsChecked
$checkbox . ToolTip = $_ . Description
$checkbox . Style = $window . Resources [ " AppsPanelCheckBoxStyle " ]
# Attach shift-click behavior for range selection
AttachShiftClickBehavior -checkbox $checkbox -appsPanel $appsPanel -lastSelectedCheckboxRef ( [ ref ] $script:AppSelectionWindowLastSelectedCheckbox )
$appsPanel . Children . Add ( $checkbox ) | Out-Null
}
# Hide loading indicator
$loadingIndicator . Visibility = 'Collapsed'
2024-08-15 22:03:48 +02:00
}
2024-06-27 23:08:43 +02:00
2026-02-01 01:41:12 +01:00
# Event handlers
$titleBar . Add_MouseLeftButtonDown ( {
$window . DragMove ( )
} )
2024-06-27 23:08:43 +02:00
2026-02-01 01:41:12 +01:00
$checkAllBox . Add_Checked ( {
foreach ( $child in $appsPanel . Children ) {
if ( $child -is [ System.Windows.Controls.CheckBox ] ) {
$child . IsChecked = $true
}
}
} )
2024-06-27 23:08:43 +02:00
2026-02-01 01:41:12 +01:00
$checkAllBox . Add_Unchecked ( {
foreach ( $child in $appsPanel . Children ) {
if ( $child -is [ System.Windows.Controls.CheckBox ] ) {
$child . IsChecked = $false
}
}
} )
2024-06-27 23:08:43 +02:00
2026-02-01 01:41:12 +01:00
$onlyInstalledBox . Add_Checked ( { LoadApps } )
$onlyInstalledBox . Add_Unchecked ( { LoadApps } )
2024-06-27 23:08:43 +02:00
2026-02-01 01:41:12 +01:00
$confirmBtn . Add_Click ( {
$selectedApps = @ ( )
foreach ( $child in $appsPanel . Children ) {
if ( $child -is [ System.Windows.Controls.CheckBox ] -and $child . IsChecked ) {
$selectedApps + = $child . Tag
}
}
2025-05-19 20:30:11 +02:00
2026-02-01 01:41:12 +01:00
# Close form without saving if no apps were selected
if ( $selectedApps . Count -eq 0 ) {
$window . Close ( )
return
}
2023-09-08 00:47:59 +02:00
2026-02-01 01:41:12 +01:00
if ( $selectedApps -contains " Microsoft.WindowsStore " -and -not $Silent ) {
$result = [ System.Windows.MessageBox ] :: Show (
'Are you sure you wish to uninstall the Microsoft Store? This app cannot easily be reinstalled.' ,
'Are you sure?' ,
[ System.Windows.MessageBoxButton ] :: YesNo ,
[ System.Windows.MessageBoxImage ] :: Warning
)
2025-09-07 15:04:29 +02:00
2026-02-01 01:41:12 +01:00
if ( $result -eq [ System.Windows.MessageBoxResult ] :: No ) {
return
}
}
2023-09-12 02:59:25 +02:00
2026-02-01 01:41:12 +01:00
SaveCustomAppsListToFile -appsList $selectedApps
2024-08-15 22:03:48 +02:00
2026-02-01 01:41:12 +01:00
$window . DialogResult = $true
} )
# Load apps after window is shown (allows UI to render first)
$window . Add_ContentRendered ( {
$window . Dispatcher . Invoke ( [ System.Windows.Threading.DispatcherPriority ] :: Background , [ action ] { LoadApps } )
} )
2024-08-15 22:03:48 +02:00
2026-02-01 01:41:12 +01:00
# Show the window and return dialog result
return $window . ShowDialog ( )
2023-12-15 13:22:35 +01:00
}
2026-02-01 01:41:12 +01:00
# Saves the provided appsList to the CustomAppsList file
function SaveCustomAppsListToFile {
2024-01-31 22:19:50 +01:00
param (
2026-02-01 01:41:12 +01:00
$appsList
2023-12-15 13:22:35 +01:00
)
2026-02-01 01:41:12 +01:00
$script:SelectedApps = $appsList
2023-12-15 13:22:35 +01:00
2026-02-01 01:41:12 +01:00
# Create file that stores selected apps if it doesn't exist
if ( -not ( Test-Path $script:CustomAppsListFilePath ) ) {
$null = New-Item $script:CustomAppsListFilePath -ItemType File
2025-12-26 20:36:51 +01:00
}
2026-02-01 01:41:12 +01:00
Set-Content -Path $script:CustomAppsListFilePath -Value $script:SelectedApps
2023-12-15 13:22:35 +01:00
}
2026-02-01 01:41:12 +01:00
# Returns a validated list of apps based on the provided appsList and the supported apps from Apps.json
function ValidateAppslist {
2024-01-31 22:19:50 +01:00
param (
2026-02-01 01:41:12 +01:00
$appsList
2023-12-15 13:22:35 +01:00
)
2026-02-01 01:41:12 +01:00
$supportedAppsList = ( GetAppsFromJson | ForEach-Object { $_ . AppId } )
$validatedAppsList = @ ( )
2024-06-27 23:08:43 +02:00
2026-02-01 01:41:12 +01:00
# Validate provided appsList against supportedAppsList
Foreach ( $app in $appsList ) {
$app = $app . Trim ( )
$appString = $app . Trim ( '*' )
if ( $supportedAppsList -notcontains $appString ) {
Write-Host " Removal of app ' $appString ' is not supported and will be skipped " -ForegroundColor Yellow
continue
}
$validatedAppsList + = $appString
2024-07-17 19:45:00 +02:00
}
2023-12-15 13:22:35 +01:00
2026-02-01 01:41:12 +01:00
return $validatedAppsList
2023-12-15 13:22:35 +01:00
}
2026-02-01 01:41:12 +01:00
# Returns list of apps from the specified file, it trims the app names and removes any comments
function ReadAppslistFromFile {
2024-01-31 22:19:50 +01:00
param (
2026-02-01 01:41:12 +01:00
$appsFilePath
2023-12-15 13:22:35 +01:00
)
2026-02-01 01:41:12 +01:00
$appsList = @ ( )
2023-12-15 13:22:35 +01:00
2026-02-01 01:41:12 +01:00
if ( -not ( Test-Path $appsFilePath ) ) {
return $appsList
2025-09-13 23:36:38 +02:00
}
2025-05-07 15:51:42 +02:00
2026-02-01 01:41:12 +01:00
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 ) {
$appId = $appData . AppId . Trim ( )
$selectedByDefault = $appData . SelectedByDefault
if ( $selectedByDefault -and $appId . length -gt 0 ) {
$appsList + = $appId
}
}
}
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
}
catch {
Write-Error " Unable to read apps list from file: $appsFilePath "
AwaitKeyToExit
2023-12-15 13:22:35 +01:00
}
2023-09-08 00:47:59 +02:00
}
2026-02-01 01:41:12 +01:00
# Read Apps.json and return list of app objects with optional filtering
function GetAppsFromJson {
param (
[ switch ] $OnlyInstalled ,
[ string ] $InstalledList = " " ,
[ switch ] $InitialCheckedFromJson
)
2023-09-08 00:47:59 +02:00
2026-02-01 01:41:12 +01:00
$apps = @ ( )
try {
$jsonContent = Get-Content -Path $script:AppsListFilePath -Raw | ConvertFrom-Json
}
catch {
Write-Error " Failed to read Apps.json: $_ "
return $apps
2025-09-27 14:19:41 +02:00
}
2025-09-28 18:21:14 +02:00
2026-02-01 01:41:12 +01:00
foreach ( $appData in $jsonContent . Apps ) {
$appId = $appData . AppId . Trim ( )
if ( $appId . length -eq 0 ) { continue }
if ( $OnlyInstalled ) {
if ( -not ( $InstalledList -like ( " * $appId * " ) ) -and -not ( Get-AppxPackage -Name $appId ) ) {
2025-12-26 20:36:51 +01:00
continue
}
2026-02-01 01:41:12 +01:00
if ( ( $appId -eq " Microsoft.Edge " ) -and -not ( $InstalledList -like " * Microsoft.Edge * " ) ) {
2025-12-26 20:36:51 +01:00
continue
}
2026-02-01 01:41:12 +01:00
}
2025-12-26 20:36:51 +01:00
2026-02-01 01:41:12 +01:00
$displayName = if ( $appData . FriendlyName ) { " $( $appData . FriendlyName ) ( $appId ) " } else { $appId }
$isChecked = if ( $InitialCheckedFromJson ) { $appData . SelectedByDefault } else { $false }
2025-12-26 20:36:51 +01:00
2026-02-01 01:41:12 +01:00
$apps + = [ PSCustomObject ] @ {
AppId = $appId
DisplayName = $displayName
IsChecked = $isChecked
Description = $appData . Description
SelectedByDefault = $appData . SelectedByDefault
2025-12-26 20:36:51 +01:00
}
}
2026-02-01 01:41:12 +01:00
return $apps
2025-12-26 20:36:51 +01:00
}
2026-02-01 01:41:12 +01:00
# Run winget list and return installed apps (sync or async)
function GetInstalledAppsViaWinget {
param (
[ int ] $TimeOut = 10 ,
[ switch ] $Async
)
2025-12-26 20:36:51 +01:00
2026-02-01 01:41:12 +01:00
if ( -not $script:WingetInstalled ) { return $null }
2025-12-26 20:36:51 +01:00
2026-02-01 01:41:12 +01:00
if ( $Async ) {
$wingetListJob = Start-Job { return winget list - -accept -source -agreements - -disable -interactivity }
return @ { Job = $wingetListJob ; StartTime = Get-Date }
}
else {
$wingetListJob = Start-Job { return winget list - -accept -source -agreements - -disable -interactivity }
$jobDone = $wingetListJob | Wait-Job -TimeOut $TimeOut
if ( -not $jobDone ) {
Remove-Job -Job $wingetListJob -Force -ErrorAction SilentlyContinue
return $null
2025-12-26 20:36:51 +01:00
}
2026-02-01 01:41:12 +01:00
$result = Receive-Job -Job $wingetListJob
Remove-Job -Job $wingetListJob -ErrorAction SilentlyContinue
return $result
2025-12-26 20:36:51 +01:00
}
2025-09-27 14:19:41 +02:00
}
2026-02-01 01:41:12 +01:00
# Removes apps specified during function call from all user accounts and from the OS image.
function RemoveApps {
param (
$appslist
)
2024-04-05 18:26:58 +02:00
2026-02-01 01:41:12 +01:00
Foreach ( $app in $appsList ) {
2026-02-06 23:44:39 +01:00
if ( $script:CancelRequested ) {
return
}
2026-02-01 01:41:12 +01:00
Write-ToConsole " Attempting to remove $app ... "
2025-12-26 20:36:51 +01:00
2026-02-01 01:41:12 +01:00
# Use WinGet only to remove OneDrive and Edge
if ( ( $app -eq " Microsoft.OneDrive " ) -or ( $app -eq " Microsoft.Edge " ) ) {
if ( $script:WingetInstalled -eq $false ) {
Write-ToConsole " WinGet is either not installed or is outdated, $app could not be removed " -ForegroundColor Red
continue
}
2025-03-05 23:39:29 +01:00
2026-02-01 01:41:12 +01:00
$appName = $app -replace '\.' , '_'
2025-03-05 23:39:29 +01:00
2026-02-01 01:41:12 +01:00
# Uninstall app via WinGet, or create a scheduled task to uninstall it later
if ( $script:Params . ContainsKey ( " User " ) ) {
RegImport " Adding scheduled task to uninstall $app for user $( GetUserName ) ... " " Uninstall_ $( $appName ) .reg "
}
elseif ( $script:Params . ContainsKey ( " Sysprep " ) ) {
RegImport " Adding scheduled task to uninstall $app after for new users... " " Uninstall_ $( $appName ) .reg "
}
else {
# Uninstall app via WinGet, with any progress indicators removed from the output
StripProgress -ScriptBlock { winget uninstall - -accept -source -agreements - -disable -interactivity - -id $app } | Tee-Object -Variable wingetOutput
2025-12-26 20:36:51 +01:00
2026-02-01 01:41:12 +01:00
If ( ( $app -eq " Microsoft.Edge " ) -and ( Select-String -InputObject $wingetOutput -Pattern " Uninstall failed with exit code " ) ) {
Write-ToConsole " Unable to uninstall Microsoft Edge via WinGet " -ForegroundColor Red
2025-05-05 22:47:31 +02:00
2026-02-06 23:45:47 +01:00
if ( $script:GuiConsoleOutput ) {
$result = [ System.Windows.MessageBox ] :: Show (
'Unable to uninstall Microsoft Edge via WinGet. Would you like to forcefully uninstall it? NOT RECOMMENDED!' ,
'Force Uninstall Microsoft Edge?' ,
[ System.Windows.MessageBoxButton ] :: YesNo ,
[ System.Windows.MessageBoxImage ] :: Warning
)
if ( $result -eq [ System.Windows.MessageBoxResult ] :: Yes ) {
Write-ToConsole " "
ForceRemoveEdge
}
}
elseif ( $ ( Read-Host -Prompt " Would you like to forcefully uninstall Microsoft Edge? NOT RECOMMENDED! (y/n) " ) -eq 'y' ) {
2026-02-01 01:41:12 +01:00
Write-ToConsole " "
ForceRemoveEdge
}
2025-08-16 01:37:43 +02:00
}
}
2025-12-26 20:36:51 +01:00
2026-02-01 01:41:12 +01:00
continue
}
2025-08-16 01:37:43 +02:00
2026-02-01 01:41:12 +01:00
# Use Remove-AppxPackage to remove all other apps
$app = '*' + $app + '*'
# Remove installed app for all existing users
try {
Get-AppxPackage -Name $app -AllUsers | Remove-AppxPackage -AllUsers -ErrorAction Continue
if ( $DebugPreference -ne " SilentlyContinue " ) {
Write-ToConsole " Removed $app for all users " -ForegroundColor DarkGray
2025-05-05 22:47:31 +02:00
}
2025-12-15 23:22:29 +01:00
}
2026-02-01 01:41:12 +01:00
catch {
if ( $DebugPreference -ne " SilentlyContinue " ) {
Write-ToConsole " Unable to remove $app for all users " -ForegroundColor Yellow
Write-Host $psitem . Exception . StackTrace -ForegroundColor Gray
}
2025-05-05 22:47:31 +02:00
}
2025-05-03 17:55:31 +02:00
2026-02-01 01:41:12 +01:00
# Remove provisioned app from OS image, so the app won't be installed for any new users
2025-05-04 22:35:58 +02:00
try {
2026-02-01 01:41:12 +01:00
Get-AppxProvisionedPackage -Online | Where-Object { $_ . PackageName -like $app } | ForEach-Object { Remove-ProvisionedAppxPackage -Online -AllUsers -PackageName $_ . PackageName }
2025-12-15 23:22:29 +01:00
}
catch {
2026-02-01 01:41:12 +01:00
Write-ToConsole " Unable to remove $app from windows image " -ForegroundColor Yellow
Write-Host $psitem . Exception . StackTrace -ForegroundColor Gray
2025-05-04 22:35:58 +02:00
}
2026-02-01 01:41:12 +01:00
}
2025-12-26 20:36:51 +01:00
2026-02-01 01:41:12 +01:00
Write-ToConsole " "
}
# Forcefully removes Microsoft Edge using its uninstaller
# Credit: Based on work from loadstring1 & ave9858
function ForceRemoveEdge {
Write-ToConsole " > Forcefully uninstalling Microsoft Edge... "
$regView = [ Microsoft.Win32.RegistryView ] :: Registry32
$hklm = [ Microsoft.Win32.RegistryKey ] :: OpenBaseKey ( [ Microsoft.Win32.RegistryHive ] :: LocalMachine , $regView )
$hklm . CreateSubKey ( 'SOFTWARE\Microsoft\EdgeUpdateDev' ) . SetValue ( 'AllowUninstall' , '' )
2026-02-06 23:45:47 +01:00
# Create stub (This somehow allows uninstalling Edge)
2026-02-01 01:41:12 +01:00
$edgeStub = " $env:SystemRoot \SystemApps\Microsoft.MicrosoftEdge_8wekyb3d8bbwe "
New-Item $edgeStub -ItemType Directory | Out-Null
New-Item " $edgeStub \MicrosoftEdge.exe " | Out-Null
# Remove edge
$uninstallRegKey = $hklm . OpenSubKey ( 'SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Microsoft Edge' )
if ( $null -ne $uninstallRegKey ) {
Write-ToConsole " Running uninstaller... "
$uninstallString = $uninstallRegKey . GetValue ( 'UninstallString' ) + ' --force-uninstall'
Start-Process cmd . exe " /c $uninstallString " -WindowStyle Hidden -Wait
Write-ToConsole " Removing leftover files... "
$edgePaths = @ (
" $env:ProgramData \Microsoft\Windows\Start Menu\Programs\Microsoft Edge.lnk " ,
" $env:APPDATA \Microsoft\Internet Explorer\Quick Launch\Microsoft Edge.lnk " ,
" $env:APPDATA \Microsoft\Internet Explorer\Quick Launch\User Pinned\TaskBar\Microsoft Edge.lnk " ,
" $env:APPDATA \Microsoft\Internet Explorer\Quick Launch\User Pinned\TaskBar\Tombstones\Microsoft Edge.lnk " ,
" $env:PUBLIC \Desktop\Microsoft Edge.lnk " ,
" $env:USERPROFILE \Desktop\Microsoft Edge.lnk " ,
" $edgeStub "
)
foreach ( $path in $edgePaths ) {
if ( Test-Path -Path $path ) {
Remove-Item -Path $path -Force -Recurse -ErrorAction SilentlyContinue
Write-ToConsole " Removed $path " -ForegroundColor DarkGray
2025-08-16 01:37:43 +02:00
}
2025-12-15 23:22:29 +01:00
}
2025-12-26 20:36:51 +01:00
2026-02-01 01:41:12 +01:00
Write-ToConsole " Cleaning up registry... "
2025-08-16 01:37:43 +02:00
2026-02-01 01:41:12 +01:00
# Remove MS Edge from autostart
reg delete " HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run " / v " MicrosoftEdgeAutoLaunch_A9F6DCE4ABADF4F51CF45CD7129E3C6C " / f * > $null
reg delete " HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run " / v " Microsoft Edge Update " / f * > $null
reg delete " HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\Run " / v " MicrosoftEdgeAutoLaunch_A9F6DCE4ABADF4F51CF45CD7129E3C6C " / f * > $null
reg delete " HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\Run " / v " Microsoft Edge Update " / f * > $null
Write-ToConsole " Microsoft Edge was uninstalled "
2025-12-15 23:22:29 +01:00
}
else {
2026-02-06 23:45:47 +01:00
Write-ToConsole " Unable to forcefully uninstall Microsoft Edge, uninstaller could not be found " -ForegroundColor Red
2025-05-03 17:55:31 +02:00
}
}
2026-02-01 01:41:12 +01:00
# Execute provided command and strips progress spinners/bars from console output
function StripProgress {
param (
[ ScriptBlock ] $ScriptBlock
)
2025-12-15 23:22:29 +01:00
2026-02-01 01:41:12 +01:00
# Regex pattern to match spinner characters and progress bar patterns
$progressPattern = 'Γû[Æê]|^\s+[-\\|/]\s+$'
2025-12-15 23:22:29 +01:00
2026-02-01 01:41:12 +01:00
# Corrected regex pattern for size formatting, ensuring proper capture groups are utilized
$sizePattern = '(\d+(\.\d{1,2})?)\s+(B|KB|MB|GB|TB|PB) /\s+(\d+(\.\d{1,2})?)\s+(B|KB|MB|GB|TB|PB)'
2025-12-15 23:22:29 +01:00
2026-02-01 01:41:12 +01:00
& $ScriptBlock 2 > & 1 | ForEach-Object {
if ( $_ -is [ System.Management.Automation.ErrorRecord ] ) {
" Error: $( $_ . Exception . Message ) "
}
else {
$line = $_ -replace $progressPattern , '' -replace $sizePattern , ''
if ( -not ( [ string ] :: IsNullOrWhiteSpace ( $line ) ) -and -not ( $line . StartsWith ( ' ' ) ) ) {
$line
}
2025-12-15 23:22:29 +01:00
}
2026-02-01 01:41:12 +01:00
}
}
2025-12-15 23:22:29 +01:00
2026-02-01 01:41:12 +01:00
# Check if this machine supports S0 Modern Standby power state. Returns true if S0 Modern Standby is supported, false otherwise.
function CheckModernStandbySupport {
$count = 0
2025-12-15 23:22:29 +01:00
2026-02-01 01:41:12 +01:00
try {
switch -Regex ( powercfg / a ) {
':' {
$count + = 1
}
2025-12-15 23:22:29 +01:00
2026-02-01 01:41:12 +01:00
'(.*S0.{1,}\))' {
if ( $count -eq 1 ) {
return $true
}
}
2025-12-15 23:22:29 +01:00
}
}
2026-02-01 01:41:12 +01:00
catch {
Write-Host " Error: Unable to check for S0 Modern Standby support, powercfg command failed " -ForegroundColor Red
Write-Host " "
Write-Host " Press any key to continue... "
$null = [ System.Console ] :: ReadKey ( )
return $true
}
2025-12-15 23:22:29 +01:00
2026-02-01 01:41:12 +01:00
return $false
2025-12-15 23:22:29 +01:00
}
2026-02-01 01:41:12 +01:00
function CheckIfUserExists {
param (
$userName
)
if ( $userName -match '[<>:"|?*]' ) {
return $false
2025-12-15 23:22:29 +01:00
}
2026-02-01 01:41:12 +01:00
if ( [ string ] :: IsNullOrWhiteSpace ( $userName ) ) {
return $false
2025-12-15 23:22:29 +01:00
}
2026-02-01 01:41:12 +01:00
try {
$userExists = Test-Path " $env:SystemDrive \Users\ $userName "
if ( $userExists ) {
return $true
2025-12-15 23:22:29 +01:00
}
2026-02-01 01:41:12 +01:00
$userExists = Test-Path ( $env:USERPROFILE -Replace ( '\\' + $env:USERNAME + '$' ) , " \ $userName " )
if ( $userExists ) {
return $true
}
}
catch {
Write-Error " Something went wrong when trying to find the user directory path for user $userName . Please ensure the user exists on this system "
2025-12-15 23:22:29 +01:00
}
2026-02-01 01:41:12 +01:00
return $false
}
# Returns the directory path of the specified user, exits script if user path can't be found
function GetUserDirectory {
param (
$userName ,
$fileName = " " ,
$exitIfPathNotFound = $true
)
2025-12-26 20:36:51 +01:00
try {
2026-02-04 10:26:55 +01:00
if ( -not ( CheckIfUserExists -userName $userName ) -and $userName -ne " * " ) {
Write-Error " User $userName does not exist on this system "
AwaitKeyToExit
}
2026-02-01 01:41:12 +01:00
$userDirectoryExists = Test-Path " $env:SystemDrive \Users\ $userName "
$userPath = " $env:SystemDrive \Users\ $userName \ $fileName "
if ( ( Test-Path $userPath ) -or ( $userDirectoryExists -and ( -not $exitIfPathNotFound ) ) ) {
return $userPath
2025-12-26 20:36:51 +01:00
}
2025-12-15 23:22:29 +01:00
2026-02-01 01:41:12 +01:00
$userDirectoryExists = Test-Path ( $env:USERPROFILE -Replace ( '\\' + $env:USERNAME + '$' ) , " \ $userName " )
$userPath = $env:USERPROFILE -Replace ( '\\' + $env:USERNAME + '$' ) , " \ $userName \ $fileName "
2025-12-15 23:22:29 +01:00
2026-02-01 01:41:12 +01:00
if ( ( Test-Path $userPath ) -or ( $userDirectoryExists -and ( -not $exitIfPathNotFound ) ) ) {
return $userPath
2025-12-15 23:22:29 +01:00
}
2026-02-01 01:41:12 +01:00
}
catch {
Write-Error " Something went wrong when trying to find the user directory path for user $userName . Please ensure the user exists on this system "
AwaitKeyToExit
}
2025-12-26 20:36:51 +01:00
2026-02-01 01:41:12 +01:00
Write-Error " Unable to find user directory path for user $userName "
AwaitKeyToExit
}
# Import & execute regfile
function RegImport {
param (
$message ,
$path
)
Write-ToConsole $message
# Validate that the regfile exists in both locations
if ( -not ( Test-Path " $script:RegfilesPath \ $path " ) -or -not ( Test-Path " $script:RegfilesPath \Sysprep\ $path " ) ) {
Write-ToConsole " Error: Unable to find registry file: $path " -ForegroundColor Red
Write-ToConsole " "
return
}
# Reset exit code before running reg.exe for reliable success detection
$global:LASTEXITCODE = 0
if ( $script:Params . ContainsKey ( " Sysprep " ) ) {
$defaultUserPath = GetUserDirectory -userName " Default " -fileName " NTUSER.DAT "
reg load " HKU\Default " $defaultUserPath | Out-Null
$regOutput = reg import " $script:RegfilesPath \Sysprep\ $path " 2 > & 1
reg unload " HKU\Default " | Out-Null
}
elseif ( $script:Params . ContainsKey ( " User " ) ) {
$userPath = GetUserDirectory -userName $script:Params . Item ( " User " ) -fileName " NTUSER.DAT "
reg load " HKU\Default " $userPath | Out-Null
$regOutput = reg import " $script:RegfilesPath \Sysprep\ $path " 2 > & 1
reg unload " HKU\Default " | Out-Null
}
else {
$regOutput = reg import " $script:RegfilesPath \ $path " 2 > & 1
}
$hasSuccess = $LASTEXITCODE -eq 0
2025-12-26 20:36:51 +01:00
2026-02-01 01:41:12 +01:00
if ( $regOutput ) {
foreach ( $line in $regOutput ) {
$lineText = if ( $line -is [ System.Management.Automation.ErrorRecord ] ) { $line . Exception . Message } else { $line . ToString ( ) }
if ( $lineText -and $lineText . Length -gt 0 ) {
if ( $hasSuccess ) {
Write-ToConsole $lineText
}
else {
Write-ToConsole $lineText -ForegroundColor Red
}
}
2025-12-15 23:22:29 +01:00
}
}
2026-02-01 01:41:12 +01:00
if ( -not $hasSuccess ) {
Write-ToConsole " Failed importing registry file: $path " -ForegroundColor Red
2025-12-15 23:22:29 +01:00
}
2026-02-01 01:41:12 +01:00
Write-ToConsole " "
}
2025-12-15 23:22:29 +01:00
2026-02-01 01:41:12 +01:00
# Restart the Windows Explorer process
function RestartExplorer {
Write-ToConsole " > Attempting to restart the Windows Explorer process to apply all changes... "
if ( $script:Params . ContainsKey ( " Sysprep " ) -or $script:Params . ContainsKey ( " User " ) -or $script:Params . ContainsKey ( " NoRestartExplorer " ) ) {
Write-ToConsole " Explorer process restart was skipped, please manually reboot your PC to apply all changes " -ForegroundColor Yellow
2025-12-26 20:36:51 +01:00
return
2025-12-15 23:22:29 +01:00
}
2026-02-01 01:41:12 +01:00
if ( $script:Params . ContainsKey ( " DisableMouseAcceleration " ) ) {
Write-ToConsole " Warning: Changes to the Enhance Pointer Precision setting will only take effect after a reboot " -ForegroundColor Yellow
}
if ( $script:Params . ContainsKey ( " DisableStickyKeys " ) ) {
Write-ToConsole " Warning: Changes to the Sticky Keys setting will only take effect after a reboot " -ForegroundColor Yellow
}
if ( $script:Params . ContainsKey ( " DisableAnimations " ) ) {
Write-ToConsole " Warning: Animations will only be disabled after a reboot " -ForegroundColor Yellow
}
# Only restart if the powershell process matches the OS architecture.
# Restarting explorer from a 32bit PowerShell window will fail on a 64bit OS
if ( [ Environment ] :: Is64BitProcess -eq [ Environment ] :: Is64BitOperatingSystem ) {
Write-ToConsole " Restarting the Windows Explorer process... (This may cause your screen to flicker) "
Stop-Process -processName: Explorer -Force
}
else {
Write-ToConsole " Unable to restart Windows Explorer process, please manually reboot your PC to apply all changes " -ForegroundColor Yellow
}
2025-12-15 23:22:29 +01:00
}
2026-02-01 01:41:12 +01:00
# Replace the startmenu for all users, when using the default startmenuTemplate this clears all pinned apps
# Credit: https://lazyadmin.nl/win-11/customize-windows-11-start-menu-layout/
function ReplaceStartMenuForAllUsers {
param (
$startMenuTemplate = " $script:AssetsPath /Start/start2.bin "
)
2025-12-15 23:22:29 +01:00
2026-02-01 01:41:12 +01:00
Write-ToConsole " > Removing all pinned apps from the start menu for all users... "
2025-12-15 23:22:29 +01:00
2026-02-01 01:41:12 +01:00
# Check if template bin file exists
if ( -not ( Test-Path $startMenuTemplate ) ) {
Write-ToConsole " Error: Unable to clear start menu, start2.bin file missing from script folder " -ForegroundColor Red
Write-ToConsole " "
return
}
2025-12-15 23:22:29 +01:00
2026-02-01 01:41:12 +01:00
# Get path to start menu file for all users
$userPathString = GetUserDirectory -userName " * " -fileName " AppData\Local\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState "
$usersStartMenuPaths = get-childitem -path $userPathString
2025-12-15 23:22:29 +01:00
2026-02-01 01:41:12 +01:00
# Go through all users and replace the start menu file
ForEach ( $startMenuPath in $usersStartMenuPaths ) {
ReplaceStartMenu $startMenuTemplate " $( $startMenuPath . Fullname ) \start2.bin "
}
2025-12-15 23:22:29 +01:00
2026-02-01 01:41:12 +01:00
# Also replace the start menu file for the default user profile
$defaultStartMenuPath = GetUserDirectory -userName " Default " -fileName " AppData\Local\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState " -exitIfPathNotFound $false
# Create folder if it doesn't exist
if ( -not ( Test-Path $defaultStartMenuPath ) ) {
new-item $defaultStartMenuPath -ItemType Directory -Force | Out-Null
Write-ToConsole " Created LocalState folder for default user profile "
2025-12-15 23:22:29 +01:00
}
2026-02-01 01:41:12 +01:00
# Copy template to default profile
Copy-Item -Path $startMenuTemplate -Destination $defaultStartMenuPath -Force
Write-ToConsole " Replaced start menu for the default user profile "
Write-ToConsole " "
2025-12-15 23:22:29 +01:00
}
2026-02-01 01:41:12 +01:00
# Replace the startmenu for all users, when using the default startmenuTemplate this clears all pinned apps
# Credit: https://lazyadmin.nl/win-11/customize-windows-11-start-menu-layout/
function ReplaceStartMenu {
param (
$startMenuTemplate = " $script:AssetsPath /Start/start2.bin " ,
$startMenuBinFile = " $env:LOCALAPPDATA \Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin "
)
2025-12-26 20:36:51 +01:00
2026-02-01 01:41:12 +01:00
# Change path to correct user if a user was specified
if ( $script:Params . ContainsKey ( " User " ) ) {
$startMenuBinFile = GetUserDirectory -userName " $( GetUserName ) " -fileName " AppData\Local\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin " -exitIfPathNotFound $false
}
2025-02-13 21:08:47 +01:00
2026-02-01 01:41:12 +01:00
# Check if template bin file exists
if ( -not ( Test-Path $startMenuTemplate ) ) {
Write-ToConsole " Error: Unable to replace start menu, template file not found " -ForegroundColor Red
return
}
2025-05-03 17:55:31 +02:00
2026-02-01 01:41:12 +01:00
if ( [ IO.Path ] :: GetExtension ( $startMenuTemplate ) -ne " .bin " ) {
Write-ToConsole " Error: Unable to replace start menu, template file is not a valid .bin file " -ForegroundColor Red
return
}
2025-02-13 21:08:47 +01:00
2026-02-01 01:41:12 +01:00
$userName = [ regex ] :: Match ( $startMenuBinFile , '(?:Users\\)([^\\]+)(?:\\AppData)' ) . Groups [ 1 ] . Value
2025-02-13 21:08:47 +01:00
2026-02-01 01:41:12 +01:00
$backupBinFile = $startMenuBinFile + " .bak "
if ( Test-Path $startMenuBinFile ) {
# Backup current start menu file
Move-Item -Path $startMenuBinFile -Destination $backupBinFile -Force
}
else {
Write-ToConsole " Unable to find original start2.bin file for user $userName , no backup was created for this user " -ForegroundColor Yellow
New-Item -ItemType File -Path $startMenuBinFile -Force
}
# Copy template file
Copy-Item -Path $startMenuTemplate -Destination $startMenuBinFile -Force
Write-ToConsole " Replaced start menu for user $userName "
}
# Add parameter to script and write to file
function AddParameter {
param (
$parameterName ,
$value = $true
)
# Add parameter or update its value if key already exists
if ( -not $script:Params . ContainsKey ( $parameterName ) ) {
$script:Params . Add ( $parameterName , $value )
}
else {
$script:Params [ $parameterName ] = $value
}
}
# Saves the current settings, excluding control parameters, to a JSON file
function SaveSettings {
$settings = @ {
" Version " = " 1.0 "
" Settings " = @ ( )
}
foreach ( $param in $script:Params . Keys ) {
if ( $script:ControlParams -notcontains $param ) {
$value = $script:Params [ $param ]
2025-02-13 21:08:47 +01:00
2026-02-01 01:41:12 +01:00
$settings . Settings + = @ {
" Name " = $param
" Value " = $value
2025-02-13 21:08:47 +01:00
}
}
}
2025-12-26 20:36:51 +01:00
2026-02-01 01:41:12 +01:00
try {
$settings | ConvertTo-Json -Depth 10 | Set-Content $script:SavedSettingsFilePath
}
catch {
Write-Output " "
Write-Host " Error: Failed to save settings to LastUsedSettings.json file " -ForegroundColor Red
}
}
2025-12-26 20:36:51 +01:00
2025-02-13 21:08:47 +01:00
2026-02-01 01:41:12 +01:00
# Prints the header for the script
function PrintHeader {
param (
$title
)
2025-02-13 21:08:47 +01:00
2026-02-01 01:41:12 +01:00
$fullTitle = " Win11Debloat Script - $title "
2025-02-13 21:08:47 +01:00
2026-02-01 01:41:12 +01:00
if ( $script:Params . ContainsKey ( " Sysprep " ) ) {
$fullTitle = " $fullTitle (Sysprep mode) "
}
else {
$fullTitle = " $fullTitle (User: $( GetUserName ) ) "
2025-02-13 21:08:47 +01:00
}
2026-02-01 01:41:12 +01:00
Clear-Host
Write-Host " ------------------------------------------------------------------------------------------- "
Write-Host $fullTitle
Write-Host " ------------------------------------------------------------------------------------------- "
}
2025-02-13 21:08:47 +01:00
2026-02-01 01:41:12 +01:00
# Prints all pending changes that will be made by the script
function PrintPendingChanges {
Write-Output " Win11Debloat will make the following changes: "
2025-02-13 21:08:47 +01:00
2026-02-01 01:41:12 +01:00
if ( $script:Params [ 'CreateRestorePoint' ] ) {
Write-Output " - $( $script:Features [ 'CreateRestorePoint' ] . Label ) "
2025-02-13 21:08:47 +01:00
}
2026-02-01 01:41:12 +01:00
foreach ( $parameterName in $script:Params . Keys ) {
if ( $script:ControlParams -contains $parameterName ) {
continue
}
2025-02-13 21:08:47 +01:00
2026-02-01 01:41:12 +01:00
# Print parameter description
switch ( $parameterName ) {
'Apps' {
continue
}
'CreateRestorePoint' {
continue
}
'RemoveApps' {
$appsList = GenerateAppsList
2025-02-13 21:08:47 +01:00
2026-02-01 01:41:12 +01:00
if ( $appsList . Count -eq 0 ) {
Write-Host " No valid apps were selected for removal " -ForegroundColor Yellow
Write-Output " "
continue
}
2025-02-13 21:08:47 +01:00
2026-02-01 01:41:12 +01:00
Write-Output " - Remove $( $appsList . Count ) apps: "
Write-Host $appsList -ForegroundColor DarkGray
continue
}
'RemoveAppsCustom' {
$appsList = ReadAppslistFromFile $script:CustomAppsListFilePath
2025-02-13 21:08:47 +01:00
2026-02-01 01:41:12 +01:00
if ( $appsList . Count -eq 0 ) {
Write-Host " No valid apps were selected for removal " -ForegroundColor Yellow
Write-Output " "
continue
}
2025-08-01 21:21:38 +02:00
2026-02-01 01:41:12 +01:00
Write-Output " - Remove $( $appsList . Count ) apps: "
Write-Host $appsList -ForegroundColor DarkGray
continue
2025-08-01 21:21:38 +02:00
}
2026-02-01 01:41:12 +01:00
default {
if ( $script:Features -and $script:Features . ContainsKey ( $parameterName ) ) {
$action = $script:Features [ $parameterName ] . Action
$message = $script:Features [ $parameterName ] . Label
Write-Output " - $action $message "
}
else {
# Fallback: show the parameter name if no feature description is available
Write-Output " - $parameterName "
}
continue
2025-08-01 21:21:38 +02:00
}
2025-02-13 21:08:47 +01:00
}
}
2025-05-20 20:38:47 +02:00
Write-Output " "
2025-06-12 21:50:57 +02:00
Write-Output " "
2026-02-01 01:41:12 +01:00
Write-Output " Press enter to execute the script or press CTRL+C to quit... "
Read-Host | Out-Null
}
2025-06-12 21:50:57 +02:00
2026-02-01 01:41:12 +01:00
# Generates a list of apps to remove based on the Apps parameter
function GenerateAppsList {
if ( -not ( $script:Params [ " Apps " ] -and $script:Params [ " Apps " ] -is [ string ] ) ) {
return @ ( )
2025-06-12 21:50:57 +02:00
}
2026-02-01 01:41:12 +01:00
$appMode = $script:Params [ " Apps " ] . toLower ( )
2025-02-13 21:08:47 +01:00
2026-02-01 01:41:12 +01:00
switch ( $appMode ) {
'default' {
$appsList = ReadAppslistFromFile $script:AppsListFilePath
return $appsList
}
default {
$appsList = $script:Params [ " Apps " ] . Split ( ',' ) | ForEach-Object { $_ . Trim ( ) }
$validatedAppsList = ValidateAppslist $appsList
return $validatedAppsList
2025-02-13 21:08:47 +01:00
}
}
2026-02-01 01:41:12 +01:00
}
2025-02-13 21:08:47 +01:00
2026-02-01 01:41:12 +01:00
function AwaitKeyToExit {
# Suppress prompt if Silent parameter was passed
if ( -not $Silent ) {
2025-04-16 12:56:37 +02:00
Write-Output " "
2026-02-01 01:41:12 +01:00
Write-Output " Press any key to exit... "
$null = [ System.Console ] :: ReadKey ( )
2025-04-16 12:56:37 +02:00
}
2026-02-01 01:41:12 +01:00
Stop-Transcript
Exit
}
2025-05-01 22:32:41 +01:00
2025-08-19 23:24:28 +05:30
2026-02-01 01:41:12 +01:00
function GetUserName {
if ( $script:Params . ContainsKey ( " User " ) ) {
return $script:Params . Item ( " User " )
2025-08-19 23:24:28 +05:30
}
2026-02-01 01:41:12 +01:00
return $env:USERNAME
}
2025-02-13 21:08:47 +01:00
2026-02-01 01:41:12 +01:00
# Executes a single parameter/feature based on its key
# Parameters:
# $paramKey - The parameter name to execute
function ExecuteParameter {
param (
[ string ] $paramKey
)
# Check if this feature has metadata in Features.json
$feature = $null
if ( $script:Features . ContainsKey ( $paramKey ) ) {
$feature = $script:Features [ $paramKey ]
}
# If feature has RegistryKey and ApplyText, use dynamic RegImport
if ( $feature -and $feature . RegistryKey -and $feature . ApplyText ) {
RegImport $feature . ApplyText $feature . RegistryKey
# Handle special cases that have additional logic after RegImport
switch ( $paramKey ) {
'DisableBing' {
# Also remove the app package for Bing search
RemoveApps 'Microsoft.BingSearch'
2025-02-13 21:08:47 +01:00
}
2026-02-01 01:41:12 +01:00
'DisableCopilot' {
# Also remove the app package for Copilot
RemoveApps 'Microsoft.Copilot'
2025-02-13 21:08:47 +01:00
}
2026-02-01 01:41:12 +01:00
'DisableWidgets' {
# Also remove the app package for Widgets
RemoveApps 'Microsoft.StartExperiencesApp'
2025-02-13 21:08:47 +01:00
}
}
2026-02-01 01:41:12 +01:00
return
2025-02-13 21:08:47 +01:00
}
2026-02-01 01:41:12 +01:00
# Handle features without RegistryKey or with special logic
switch ( $paramKey ) {
'RemoveApps' {
Write-ToConsole " > Removing selected apps... "
$appsList = GenerateAppsList
if ( $appsList . Count -eq 0 ) {
Write-ToConsole " No valid apps were selected for removal " -ForegroundColor Yellow
Write-ToConsole " "
return
2025-02-13 21:08:47 +01:00
}
2026-02-01 01:41:12 +01:00
Write-ToConsole " $( $appsList . Count ) apps selected for removal "
RemoveApps $appsList
}
'RemoveAppsCustom' {
Write-ToConsole " > Removing selected apps... "
$appsList = ReadAppslistFromFile $script:CustomAppsListFilePath
2025-02-13 21:08:47 +01:00
2026-02-01 01:41:12 +01:00
if ( $appsList . Count -eq 0 ) {
Write-ToConsole " No valid apps were selected for removal " -ForegroundColor Yellow
Write-ToConsole " "
return
2025-02-13 21:08:47 +01:00
}
2025-06-11 22:04:17 +02:00
2026-02-01 01:41:12 +01:00
Write-ToConsole " $( $appsList . Count ) apps selected for removal "
RemoveApps $appsList
}
'RemoveCommApps' {
$appsList = 'Microsoft.windowscommunicationsapps' , 'Microsoft.People'
Write-ToConsole " > Removing Mail, Calendar and People apps... "
RemoveApps $appsList
return
}
'RemoveW11Outlook' {
$appsList = 'Microsoft.OutlookForWindows'
Write-ToConsole " > Removing new Outlook for Windows app... "
RemoveApps $appsList
return
}
'RemoveGamingApps' {
$appsList = 'Microsoft.GamingApp' , 'Microsoft.XboxGameOverlay' , 'Microsoft.XboxGamingOverlay'
Write-ToConsole " > Removing gaming related apps... "
RemoveApps $appsList
return
}
'RemoveHPApps' {
$appsList = 'AD2F1837.HPAIExperienceCenter' , 'AD2F1837.HPJumpStarts' , 'AD2F1837.HPPCHardwareDiagnosticsWindows' , 'AD2F1837.HPPowerManager' , 'AD2F1837.HPPrivacySettings' , 'AD2F1837.HPSupportAssistant' , 'AD2F1837.HPSureShieldAI' , 'AD2F1837.HPSystemInformation' , 'AD2F1837.HPQuickDrop' , 'AD2F1837.HPWorkWell' , 'AD2F1837.myHP' , 'AD2F1837.HPDesktopSupportUtilities' , 'AD2F1837.HPQuickTouch' , 'AD2F1837.HPEasyClean' , 'AD2F1837.HPConnectedMusic' , 'AD2F1837.HPFileViewer' , 'AD2F1837.HPRegistration' , 'AD2F1837.HPWelcome' , 'AD2F1837.HPConnectedPhotopoweredbySnapfish' , 'AD2F1837.HPPrinterControl'
Write-ToConsole " > Removing HP apps... "
RemoveApps $appsList
return
}
" ForceRemoveEdge " {
ForceRemoveEdge
return
}
'ClearStart' {
Write-ToConsole " > Removing all pinned apps from the start menu for user $( GetUserName ) ... "
ReplaceStartMenu
Write-ToConsole " "
return
}
'ReplaceStart' {
Write-ToConsole " > Replacing the start menu for user $( GetUserName ) ... "
ReplaceStartMenu $script:Params . Item ( " ReplaceStart " )
Write-ToConsole " "
return
}
'ClearStartAllUsers' {
ReplaceStartMenuForAllUsers
return
}
'ReplaceStartAllUsers' {
ReplaceStartMenuForAllUsers $script:Params . Item ( " ReplaceStartAllUsers " )
return
2025-02-13 21:08:47 +01:00
}
}
2026-02-01 01:41:12 +01:00
}
2025-02-13 21:08:47 +01:00
2026-02-01 01:41:12 +01:00
# Executes all selected parameters/features
# Parameters:
function ExecuteAllChanges {
# Create restore point if requested (CLI only - GUI handles this separately)
if ( $script:Params . ContainsKey ( " CreateRestorePoint " ) ) {
Write-ToConsole " > Attempting to create a system restore point... "
CreateSystemRestorePoint
2026-02-06 23:45:47 +01:00
Write-ToConsole " "
2026-02-01 01:41:12 +01:00
}
# Execute all parameters
foreach ( $paramKey in $script:Params . Keys ) {
2026-02-06 23:44:39 +01:00
if ( $script:CancelRequested ) {
return
}
2026-02-01 01:41:12 +01:00
if ( $script:ControlParams -contains $paramKey ) {
continue
}
ExecuteParameter -paramKey $paramKey
}
}
2025-02-13 21:08:47 +01:00
2026-02-01 01:41:12 +01:00
function CreateSystemRestorePoint {
$SysRestore = Get-ItemProperty -Path " HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\SystemRestore " -Name " RPSessionInterval "
2025-02-13 21:08:47 +01:00
2026-02-01 01:41:12 +01:00
if ( $SysRestore . RPSessionInterval -eq 0 ) {
# In GUI mode, skip the prompt and just try to enable it
if ( $script:GuiConsoleOutput -or $Silent -or $ ( Read-Host -Prompt " System restore is disabled, would you like to enable it and create a restore point? (y/n) " ) -eq 'y' ) {
$enableSystemRestoreJob = Start-Job {
try {
Enable-ComputerRestore -Drive " $env:SystemDrive "
2025-02-13 21:08:47 +01:00
}
2026-02-01 01:41:12 +01:00
catch {
return " Error: Failed to enable System Restore: $_ "
2025-02-13 21:08:47 +01:00
}
2026-02-01 01:41:12 +01:00
return $null
2025-02-13 21:08:47 +01:00
}
2026-02-01 01:41:12 +01:00
$enableSystemRestoreJobDone = $enableSystemRestoreJob | Wait-Job -TimeOut 20
2025-02-13 21:08:47 +01:00
2026-02-01 01:41:12 +01:00
if ( -not $enableSystemRestoreJobDone ) {
Remove-Job -Job $enableSystemRestoreJob -Force -ErrorAction SilentlyContinue
Write-ToConsole " Error: Failed to enable system restore and create restore point, operation timed out " -ForegroundColor Red
return
}
else {
$result = Receive-Job $enableSystemRestoreJob
Remove-Job -Job $enableSystemRestoreJob -ErrorAction SilentlyContinue
if ( $result ) {
Write-ToConsole $result -ForegroundColor Red
return
}
2025-02-13 21:08:47 +01:00
}
}
2026-02-01 01:41:12 +01:00
else {
Write-ToConsole " "
return
}
}
2025-02-13 21:08:47 +01:00
2026-02-01 01:41:12 +01:00
$createRestorePointJob = Start-Job {
# Find existing restore points that are less than 24 hours old
try {
$recentRestorePoints = Get-ComputerRestorePoint | Where-Object { ( Get-Date ) - [ System.Management.ManagementDateTimeConverter ] :: ToDateTime ( $_ . CreationTime ) -le ( New-TimeSpan -Hours 24 ) }
}
catch {
return @ { Success = $false ; Message = " Error: Unable to retrieve existing restore points: $_ " }
}
2025-02-13 21:08:47 +01:00
2026-02-01 01:41:12 +01:00
if ( $recentRestorePoints . Count -eq 0 ) {
try {
Checkpoint-Computer -Description " Restore point created by Win11Debloat " -RestorePointType " MODIFY_SETTINGS "
return @ { Success = $true ; Message = " System restore point created successfully " }
}
catch {
return @ { Success = $false ; Message = " Error: Unable to create restore point: $_ " }
}
}
else {
return @ { Success = $true ; Message = " A recent restore point already exists, no new restore point was created " ; Warning = $true }
2025-02-13 21:08:47 +01:00
}
2026-02-01 01:41:12 +01:00
}
2025-02-13 21:08:47 +01:00
2026-02-01 01:41:12 +01:00
$createRestorePointJobDone = $createRestorePointJob | Wait-Job -TimeOut 20
2025-02-13 21:08:47 +01:00
2026-02-01 01:41:12 +01:00
if ( -not $createRestorePointJobDone ) {
Remove-Job -Job $createRestorePointJob -Force -ErrorAction SilentlyContinue
Write-ToConsole " Error: Failed to create system restore point, operation timed out " -ForegroundColor Red
}
else {
$result = Receive-Job $createRestorePointJob
Remove-Job -Job $createRestorePointJob -ErrorAction SilentlyContinue
if ( $result . Success ) {
if ( $result . Warning ) {
Write-ToConsole $result . Message -ForegroundColor Yellow
}
else {
Write-ToConsole $result . Message
2025-02-13 21:08:47 +01:00
}
}
2026-02-01 01:41:12 +01:00
else {
Write-ToConsole $result . Message -ForegroundColor Red
}
}
}
2025-04-16 12:31:18 +02:00
2025-12-26 20:36:51 +01:00
2026-02-01 01:41:12 +01:00
function ShowScriptMenuOptions {
Do {
$ModeSelectionMessage = " Please select an option (1/2) "
2025-12-29 01:03:14 +01:00
2026-02-01 01:41:12 +01:00
PrintHeader 'Menu'
2025-12-29 01:03:14 +01:00
2026-02-01 01:41:12 +01:00
Write-Host " (1) Default mode: Quickly apply the recommended changes "
Write-Host " (2) App removal mode: Select & remove apps, without making other changes "
2025-12-29 01:03:14 +01:00
2026-02-01 01:41:12 +01:00
# Only show this option if SavedSettings file exists
if ( Test-Path $script:SavedSettingsFilePath ) {
Write-Host " (3) Quickly apply your last used settings "
$ModeSelectionMessage = " Please select an option (1/2/3) "
2025-12-29 01:03:14 +01:00
}
2025-02-13 21:08:47 +01:00
2026-02-01 01:41:12 +01:00
Write-Host " "
Write-Host " "
2025-02-13 21:08:47 +01:00
2026-02-01 01:41:12 +01:00
$Mode = Read-Host $ModeSelectionMessage
if ( ( $Mode -eq '3' ) -and -not ( Test-Path $script:SavedSettingsFilePath ) ) {
$Mode = $null
2025-02-13 21:08:47 +01:00
}
2026-02-01 01:41:12 +01:00
}
while ( $Mode -ne '1' -and $Mode -ne '2' -and $Mode -ne '3' )
2025-02-13 21:08:47 +01:00
2026-02-01 01:41:12 +01:00
return $Mode
}
2025-02-13 21:08:47 +01:00
2026-02-01 01:41:12 +01:00
function ShowDefaultModeOptions {
# Show options for removing apps, or set selection if RunDefaults or RunDefaultsLite parameter was passed
if ( $RunDefaults ) {
$RemoveAppsInput = '1'
}
elseif ( $RunDefaultsLite ) {
$RemoveAppsInput = '0'
}
else {
$RemoveAppsInput = ShowDefaultModeAppRemovalOptions
2025-02-13 21:08:47 +01:00
2026-02-01 01:41:12 +01:00
if ( $RemoveAppsInput -eq '2' -and ( $script:SelectedApps . contains ( 'Microsoft.XboxGameOverlay' ) -or $script:SelectedApps . contains ( 'Microsoft.XboxGamingOverlay' ) ) -and
$ ( Read-Host -Prompt " Disable Game Bar integration and game/screen recording? This also stops ms-gamingoverlay and ms-gamebar popups (y/n) " ) -eq 'y' ) {
$DisableGameBarIntegrationInput = $true ;
2025-02-13 21:08:47 +01:00
}
2026-02-01 01:41:12 +01:00
}
2025-02-13 21:08:47 +01:00
2026-02-01 01:41:12 +01:00
PrintHeader 'Default Mode'
2025-02-13 21:08:47 +01:00
2026-02-01 01:41:12 +01:00
# Add default settings based on user input
try {
# Select app removal options based on user input
switch ( $RemoveAppsInput ) {
'1' {
AddParameter 'RemoveApps'
AddParameter 'Apps' 'Default'
2025-12-29 01:03:14 +01:00
}
2026-02-01 01:41:12 +01:00
'2' {
AddParameter 'RemoveAppsCustom'
2025-12-29 01:03:14 +01:00
2026-02-01 01:41:12 +01:00
if ( $DisableGameBarIntegrationInput ) {
AddParameter 'DisableDVR'
AddParameter 'DisableGameBarIntegration'
}
2025-02-13 21:08:47 +01:00
}
2026-02-01 01:41:12 +01:00
}
2025-02-13 21:08:47 +01:00
2026-02-01 01:41:12 +01:00
# Load settings from DefaultSettings.json and add to params
LoadSettingsToParams -filePath $script:DefaultSettingsFilePath -expectedVersion " 1.0 "
}
catch {
Write-Error " Failed to load settings from DefaultSettings.json file: $_ "
AwaitKeyToExit
}
2025-02-13 21:08:47 +01:00
2026-02-01 01:41:12 +01:00
SaveSettings
2025-02-13 21:08:47 +01:00
2026-02-01 01:41:12 +01:00
# Skip change summary if Silent parameter was passed
if ( $Silent ) {
return
}
2025-02-13 21:08:47 +01:00
2026-02-01 01:41:12 +01:00
PrintPendingChanges
PrintHeader 'Default Mode'
}
2025-02-13 21:08:47 +01:00
2026-02-01 01:41:12 +01:00
function ShowDefaultModeAppRemovalOptions {
PrintHeader 'Default Mode'
2025-02-13 21:08:47 +01:00
2026-02-01 01:41:12 +01:00
Write-Host " Please note: The default selection of apps includes Microsoft Teams, Spotify, Sticky Notes and more. Select option 2 to verify and change what apps are removed by the script " -ForegroundColor DarkGray
Write-Host " "
2025-02-13 21:08:47 +01:00
2026-02-01 01:41:12 +01:00
Do {
Write-Host " Options: " -ForegroundColor Yellow
Write-Host " (n) Don't remove any apps " -ForegroundColor Yellow
Write-Host " (1) Only remove the default selection of apps " -ForegroundColor Yellow
Write-Host " (2) Manually select which apps to remove " -ForegroundColor Yellow
$RemoveAppsInput = Read-Host " Do you want to remove any apps? Apps will be removed for all users (n/1/2) "
2025-12-26 20:36:51 +01:00
2026-02-01 01:41:12 +01:00
# Show app selection form if user entered option 3
if ( $RemoveAppsInput -eq '2' ) {
$result = OpenAppSelectionWindow
2025-12-26 20:36:51 +01:00
2026-02-01 01:41:12 +01:00
if ( $result -ne $true ) {
# User cancelled or closed app selection, change RemoveAppsInput so the menu will be shown again
Write-Host " "
Write-Host " Cancelled application selection, please try again " -ForegroundColor Red
2025-02-13 21:08:47 +01:00
2026-02-01 01:41:12 +01:00
$RemoveAppsInput = 'c'
2025-02-13 21:08:47 +01:00
}
2026-02-01 01:41:12 +01:00
Write-Host " "
2025-02-13 21:08:47 +01:00
}
}
2026-02-01 01:41:12 +01:00
while ( $RemoveAppsInput -ne 'n' -and $RemoveAppsInput -ne '0' -and $RemoveAppsInput -ne '1' -and $RemoveAppsInput -ne '2' )
2025-02-13 21:08:47 +01:00
2026-02-01 01:41:12 +01:00
return $RemoveAppsInput
2025-02-13 21:08:47 +01:00
}
2025-12-15 23:22:29 +01:00
function ShowAppRemoval {
PrintHeader " App Removal "
2025-12-26 20:36:51 +01:00
Write-Output " > Opening app selection form... "
2026-02-01 01:41:12 +01:00
$result = OpenAppSelectionWindow
2025-12-15 23:22:29 +01:00
2026-02-01 01:41:12 +01:00
if ( $result -eq $true ) {
2025-12-15 23:22:29 +01:00
Write-Output " You have selected $( $script:SelectedApps . Count ) apps for removal "
2025-12-26 20:36:51 +01:00
AddParameter 'RemoveAppsCustom'
SaveSettings
2025-12-15 23:22:29 +01:00
# Suppress prompt if Silent parameter was passed
if ( -not $Silent ) {
Write-Output " "
Write-Output " "
Write-Output " Press enter to remove the selected apps or press CTRL+C to quit... "
Read-Host | Out-Null
PrintHeader " App Removal "
}
}
else {
Write-Host " Selection was cancelled, no apps have been removed " -ForegroundColor Red
Write-Output " "
}
}
2025-12-26 20:36:51 +01:00
function LoadAndShowLastUsedSettings {
2025-12-15 23:22:29 +01:00
PrintHeader 'Custom Mode'
2025-12-26 20:36:51 +01:00
try {
2026-02-01 01:41:12 +01:00
# Load settings from LastUsedSettings.json and add to params
LoadSettingsToParams -filePath $script:SavedSettingsFilePath -expectedVersion " 1.0 "
2025-12-15 23:22:29 +01:00
}
2025-12-26 20:36:51 +01:00
catch {
2026-02-01 01:41:12 +01:00
Write-Error " Failed to load settings from LastUsedSettings.json file: $_ "
2025-12-26 20:36:51 +01:00
AwaitKeyToExit
2025-12-15 23:22:29 +01:00
}
2025-12-26 20:36:51 +01:00
PrintPendingChanges
2025-12-15 23:22:29 +01:00
PrintHeader 'Custom Mode'
}
2024-06-27 23:08:43 +02:00
##################################################################################################################
# #
# SCRIPT START #
# #
##################################################################################################################
2026-02-01 01:41:12 +01:00
# Get current Windows build version
2024-06-27 23:08:43 +02:00
$WinVersion = Get-ItemPropertyValue 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' CurrentBuild
2025-08-22 17:50:47 +02:00
# Check if the machine supports Modern Standby, this is used to determine if the DisableModernStandbyNetworking option can be used
$script:ModernStandbySupported = CheckModernStandbySupport
2025-08-19 23:24:28 +05:30
2025-05-10 13:02:48 +02:00
$script:Params = $PSBoundParameters
2023-09-27 21:03:25 +02:00
2025-12-26 20:36:51 +01:00
# Add default Apps parameter when RemoveApps is requested and Apps was not explicitly provided
if ( ( -not $script:Params . ContainsKey ( " Apps " ) ) -and $script:Params . ContainsKey ( " RemoveApps " ) ) {
$script:Params . Add ( 'Apps' , 'Default' )
}
$controlParamsCount = 0
# Count how many control parameters are set, to determine if any changes were selected by the user during runtime
foreach ( $Param in $script:ControlParams ) {
2025-05-10 13:02:48 +02:00
if ( $script:Params . ContainsKey ( $Param ) ) {
2025-12-26 20:36:51 +01:00
$controlParamsCount + +
2023-09-27 21:03:25 +02:00
}
}
2024-08-17 00:08:09 +02:00
# Hide progress bars for app removal, as they block Win11Debloat's output
2025-05-10 13:02:48 +02:00
if ( -not ( $script:Params . ContainsKey ( " Verbose " ) ) ) {
2024-08-17 00:08:09 +02:00
$ProgressPreference = 'SilentlyContinue'
}
else {
2025-05-07 15:51:01 +02:00
Write-Host " Verbose mode is enabled "
Write-Output " "
Write-Output " Press any key to continue... "
$null = [ System.Console ] :: ReadKey ( )
2024-08-17 00:08:09 +02:00
$ProgressPreference = 'Continue'
}
2025-05-10 13:02:48 +02:00
if ( $script:Params . ContainsKey ( " Sysprep " ) ) {
2025-09-07 15:04:29 +02:00
$defaultUserPath = GetUserDirectory -userName " Default "
2024-08-31 22:13:59 +02:00
2024-06-27 23:08:43 +02:00
# Exit script if run in Sysprep mode on Windows 10
if ( $WinVersion -lt 22000 ) {
2025-12-26 20:36:51 +01:00
Write-Error " Win11Debloat Sysprep mode is not supported on Windows 10 "
2024-06-27 23:08:43 +02:00
AwaitKeyToExit
}
}
2025-03-05 23:39:29 +01:00
# Make sure all requirements for User mode are met, if User is specified
2025-05-10 13:02:48 +02:00
if ( $script:Params . ContainsKey ( " User " ) ) {
2025-09-07 15:04:29 +02:00
$userPath = GetUserDirectory -userName $script:Params . Item ( " User " )
2025-03-05 23:39:29 +01:00
}
2025-12-26 20:36:51 +01:00
# Remove LastUsedSettings.json file if it exists and is empty
if ( ( Test-Path $script:SavedSettingsFilePath ) -and ( [ String ] :: IsNullOrWhiteSpace ( ( Get-content $script:SavedSettingsFilePath ) ) ) ) {
Remove-Item -Path $script:SavedSettingsFilePath -recurse
2023-12-15 13:22:35 +01:00
}
2025-04-19 15:16:47 +02:00
# Only run the app selection form if the 'RunAppsListGenerator' parameter was passed to the script
2026-02-01 01:41:12 +01:00
if ( $RunAppsListGenerator ) {
2025-04-19 15:16:47 +02:00
PrintHeader " Custom Apps List Generator "
2024-03-11 23:38:19 +01:00
2026-02-01 01:41:12 +01:00
$result = OpenAppSelectionWindow
2024-03-29 16:56:29 +01:00
2024-03-11 23:38:19 +01:00
# Show different message based on whether the app selection was saved or cancelled
2026-02-01 01:41:12 +01:00
if ( $result -ne $true ) {
2025-04-19 15:16:47 +02:00
Write-Host " Application selection window was closed without saving. " -ForegroundColor Red
2024-03-11 23:38:19 +01:00
}
else {
2025-04-14 21:02:58 +02:00
Write-Output " Your app selection was saved to the 'CustomAppsList' file, found at: "
Write-Host " $PSScriptRoot " -ForegroundColor Yellow
2024-03-11 23:38:19 +01:00
}
2024-04-05 18:26:58 +02:00
AwaitKeyToExit
2024-03-11 23:38:19 +01:00
}
2023-08-07 00:49:41 +02:00
# Change script execution based on provided parameters or user input
2025-12-26 20:36:51 +01:00
if ( ( -not $script:Params . Count ) -or $RunDefaults -or $RunDefaultsLite -or $RunSavedSettings -or ( $controlParamsCount -eq $script:Params . Count ) ) {
2025-09-13 23:36:38 +02:00
if ( $RunDefaults -or $RunDefaultsLite ) {
2026-02-01 01:41:12 +01:00
ShowDefaultModeOptions
2020-11-07 02:57:38 +01:00
}
2025-01-09 21:33:28 +01:00
elseif ( $RunSavedSettings ) {
2025-12-26 20:36:51 +01:00
if ( -not ( Test-Path $script:SavedSettingsFilePath ) ) {
2025-01-09 21:33:28 +01:00
PrintHeader 'Custom Mode'
2025-12-26 20:36:51 +01:00
Write-Error " Unable to find LastUsedSettings.json file, no changes were made "
2025-01-09 21:33:28 +01:00
AwaitKeyToExit
}
2026-02-01 01:41:12 +01:00
LoadAndShowLastUsedSettings
2025-01-09 21:33:28 +01:00
}
2023-05-15 16:38:11 -06:00
else {
2026-02-01 01:41:12 +01:00
if ( $CLI ) {
$Mode = ShowScriptMenuOptions
}
else {
try {
$result = OpenGUI
Stop-Transcript
Exit
}
catch {
Write-Warning " Unable to load WPF GUI (not supported in this environment), falling back to CLI mode "
2026-02-01 13:12:32 +01:00
if ( -not $Silent ) {
Write-Host " "
Write-Host " Press any key to continue... "
$null = [ System.Console ] :: ReadKey ( )
}
2026-02-01 01:41:12 +01:00
$Mode = ShowScriptMenuOptions
}
}
2020-11-07 02:57:38 +01:00
}
2020-11-06 12:48:13 +01:00
2022-04-09 18:52:16 +02:00
# Add execution parameters based on the mode
2023-05-15 16:38:11 -06:00
switch ( $Mode ) {
2025-12-15 23:22:29 +01:00
# Default mode, loads defaults and app removal options
2023-05-15 16:38:11 -06:00
'1' {
2025-12-26 20:36:51 +01:00
ShowDefaultModeOptions
2021-12-15 21:45:48 +01:00
}
2022-09-08 17:20:53 +02:00
2024-03-29 16:56:29 +01:00
# App removal, remove apps based on user selection
2026-02-01 01:41:12 +01:00
'2' {
2025-12-15 23:22:29 +01:00
ShowAppRemoval
2024-03-29 16:56:29 +01:00
}
2025-12-26 20:36:51 +01:00
# Load last used options from the "LastUsedSettings.json" file
2026-02-01 01:41:12 +01:00
'3' {
2025-12-26 20:36:51 +01:00
LoadAndShowLastUsedSettings
2020-11-07 02:57:38 +01:00
}
}
2020-11-06 12:48:13 +01:00
}
2023-05-15 16:38:11 -06:00
else {
2026-02-01 01:41:12 +01:00
PrintHeader 'Configuration'
2020-11-06 12:48:13 +01:00
}
2025-12-26 20:36:51 +01:00
# If the number of keys in ControlParams equals the number of keys in Params then no modifications/changes were selected
2024-03-01 13:54:02 +01:00
# or added by the user, and the script can exit without making any changes.
2025-12-26 20:36:51 +01:00
if ( ( $controlParamsCount -eq $script:Params . Keys . Count ) -or ( $script:Params . Keys . Count -eq 1 -and ( $script:Params . Keys -contains 'CreateRestorePoint' -or $script:Params . Keys -contains 'Apps' ) ) ) {
2023-09-28 16:11:04 +02:00
Write-Output " The script completed without making any changes. "
2024-04-05 18:26:58 +02:00
AwaitKeyToExit
2020-10-27 23:26:39 +01:00
}
2025-02-13 21:08:47 +01:00
2026-02-01 01:41:12 +01:00
# Execute all selected/provided parameters using the consolidated function
# (This also handles restore point creation if requested)
ExecuteAllChanges
2023-09-27 21:03:25 +02:00
2025-05-04 23:33:20 +02:00
RestartExplorer
2023-09-27 21:03:25 +02:00
2025-05-04 23:33:20 +02:00
Write-Output " "
Write-Output " "
Write-Output " "
Write-Output " Script completed! Please check above for any errors. "
AwaitKeyToExit