# List of undo actions to execute after forward changes. # Each entry is a PSCustomObject with FeatureId and UndoRegFile (filename, without folder prefix). $script:UndoRegistryKeys = @() # List of undo actions for features that do not use registry undo files. # Each entry is a PSCustomObject with FeatureId. $script:UndoFeatureActions = @() # Resolves the path of an undo reg file relative to $script:RegfilesPath. # Checks the Undo/ subfolder first, then falls back to the root Regfiles/ folder. function Resolve-UndoRegFilePath { param ([string]$FileName) $undoSubPath = Join-Path 'Undo' $FileName if (Test-Path (Join-Path $script:RegfilesPath $undoSubPath)) { return $undoSubPath } return $FileName } function Invoke-UndoFeatureAction { param( [Parameter(Mandatory)] [string]$FeatureId ) switch ($FeatureId) { 'DisableStoreSearchSuggestions' { if ($script:Params.ContainsKey('Sysprep')) { Write-Host "> Re-enabling Microsoft Store search suggestions in the start menu for all users..." EnableStoreSearchSuggestionsForAllUsers Write-Host "" return } Write-Host "> Re-enabling Microsoft Store search suggestions for user $(GetUserName)..." EnableStoreSearchSuggestions Write-Host "" return } 'EnableWindowsSandbox' { Write-Host "> Disabling Windows Sandbox..." DisableWindowsFeature 'Containers-DisposableClientVM' Write-Host "" return } 'EnableWindowsSubsystemForLinux' { Write-Host "> Disabling Windows Subsystem for Linux..." DisableWindowsFeature 'Microsoft-Windows-Subsystem-Linux' DisableWindowsFeature 'VirtualMachinePlatform' Write-Host "" return } } } # 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 ImportRegistryFile if ($feature -and $feature.RegistryKey -and $feature.ApplyText) { ImportRegistryFile "> $($feature.ApplyText)" $feature.RegistryKey # Handle special cases that have additional logic after ImportRegistryFile switch ($paramKey) { 'DisableBing' { # Also remove the app package for Bing search RemoveApps 'Microsoft.BingSearch' } 'DisableCopilot' { # Also remove the app package for Copilot RemoveApps 'Microsoft.Copilot' } } return } # Handle features without RegistryKey or with special logic switch ($paramKey) { 'RemoveApps' { Write-Host "> Removing selected apps for $(GetFriendlyTargetUserName)..." $appsList = GenerateAppsList if ($appsList.Count -eq 0) { Write-Host "No valid apps were selected for removal" -ForegroundColor Yellow Write-Host "" return } Write-Host "$($appsList.Count) apps selected for removal" RemoveApps $appsList } 'RemoveAppsCustom' { Write-Host "> Removing selected apps..." $appsList = LoadAppsFromFile $script:CustomAppsListFilePath if ($appsList.Count -eq 0) { Write-Host "No valid apps were selected for removal" -ForegroundColor Yellow Write-Host "" return } Write-Host "$($appsList.Count) apps selected for removal" RemoveApps $appsList } 'RemoveCommApps' { $appsList = 'Microsoft.windowscommunicationsapps', 'Microsoft.People' Write-Host "> Removing Mail, Calendar and People apps..." RemoveApps $appsList return } 'RemoveW11Outlook' { $appsList = 'Microsoft.OutlookForWindows' Write-Host "> Removing new Outlook for Windows app..." RemoveApps $appsList return } 'RemoveGamingApps' { $appsList = 'Microsoft.GamingApp', 'Microsoft.XboxGameOverlay', 'Microsoft.XboxGamingOverlay' Write-Host "> 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-Host "> Removing HP apps..." RemoveApps $appsList return } 'DisableWidgets' { Write-Host "> Disabling widgets on the taskbar & lock screen..." # Stop widgets related processes before removing the app packages to prevent potential issues Get-Process *Widget* | Stop-Process RemoveApps 'Microsoft.StartExperiencesApp','MicrosoftWindows.Client.WebExperience','Microsoft.WidgetsPlatformRuntime' } "EnableWindowsSandbox" { Write-Host "> Enabling Windows Sandbox..." EnableWindowsFeature "Containers-DisposableClientVM" Write-Host "" return } "EnableWindowsSubsystemForLinux" { Write-Host "> Enabling Windows Subsystem for Linux..." EnableWindowsFeature "VirtualMachinePlatform" EnableWindowsFeature "Microsoft-Windows-Subsystem-Linux" Write-Host "" return } 'ClearStart' { Write-Host "> Removing all pinned apps from the start menu for user $(GetUserName)..." ReplaceStartMenu Write-Host "" return } 'ReplaceStart' { Write-Host "> Replacing the start menu for user $(GetUserName)..." ReplaceStartMenu $script:Params.Item("ReplaceStart") Write-Host "" return } 'ClearStartAllUsers' { ReplaceStartMenuForAllUsers return } 'ReplaceStartAllUsers' { ReplaceStartMenuForAllUsers $script:Params.Item("ReplaceStartAllUsers") return } 'DisableStoreSearchSuggestions' { if ($script:Params.ContainsKey("Sysprep")) { Write-Host "> Disabling Microsoft Store search suggestions in the start menu for all users..." DisableStoreSearchSuggestionsForAllUsers Write-Host "" return } Write-Host "> Disabling Microsoft Store search suggestions for user $(GetUserName)..." DisableStoreSearchSuggestions Write-Host "" return } } } # Executes all selected parameters/features function ExecuteAllChanges { $script:RegistryImportFailures = 0 # Build list of actionable parameters (skip control params and data-only params) $actionableKeys = @() foreach ($paramKey in $script:Params.Keys) { if ($script:ControlParams -contains $paramKey) { continue } if ($paramKey -eq 'Apps') { continue } if ($paramKey -eq 'CreateRestorePoint') { continue } $actionableKeys += $paramKey } $hasRegistryBackedFeature = $false foreach ($paramKey in $actionableKeys) { if (-not $script:Features.ContainsKey($paramKey)) { continue } $feature = $script:Features[$paramKey] if ($feature -and -not [string]::IsNullOrWhiteSpace([string]$feature.RegistryKey)) { $hasRegistryBackedFeature = $true break } } # Undo operations that write registry values also require a backup if (-not $hasRegistryBackedFeature -and $script:UndoRegistryKeys.Count -gt 0) { $hasRegistryBackedFeature = $true } $totalSteps = $actionableKeys.Count + $script:UndoRegistryKeys.Count + $script:UndoFeatureActions.Count if ($hasRegistryBackedFeature) { $totalSteps++ } if ($script:Params.ContainsKey("CreateRestorePoint")) { $totalSteps++ } $currentStep = 0 if ($hasRegistryBackedFeature) { $currentStep++ if ($script:ApplyProgressCallback) { & $script:ApplyProgressCallback $currentStep $totalSteps "Creating registry backup..." } Write-Host "> Creating registry backup..." try { $undoSyntheticFeatures = @($script:UndoRegistryKeys | ForEach-Object { [PSCustomObject]@{ FeatureId = $_.FeatureId; RegistryKey = (Resolve-UndoRegFilePath $_.UndoRegFile) } }) New-RegistrySettingsBackup -ActionableKeys $actionableKeys -ExtraFeatures $undoSyntheticFeatures | Out-Null } catch { throw "Registry backup failed before applying changes. $($_.Exception.Message)" } } # Create restore point if requested (CLI only - GUI handles this separately) if ($script:Params.ContainsKey("CreateRestorePoint")) { $currentStep++ if ($script:ApplyProgressCallback) { & $script:ApplyProgressCallback $currentStep $totalSteps "Creating system restore point, this may take a moment..." } Write-Host "> Creating a system restore point..." CreateSystemRestorePoint Write-Host "" } # Execute all parameters foreach ($paramKey in $actionableKeys) { if ($script:CancelRequested) { return } $currentStep++ # Get friendly name for the step $stepName = $paramKey if ($script:Features.ContainsKey($paramKey)) { $feature = $script:Features[$paramKey] if ($feature.ApplyText) { # Prefer explicit ApplyText when provided $stepName = $feature.ApplyText } elseif ($feature.Label) { # Fallback: use label from Features.json $stepName = $feature.Label } } if ($script:ApplyProgressCallback) { & $script:ApplyProgressCallback $currentStep $totalSteps $stepName } ExecuteParameter -paramKey $paramKey } # Execute all undo operations foreach ($undoAction in $script:UndoRegistryKeys) { if ($script:CancelRequested) { return } $undoLabel = if ($script:FeatureLabelLookup) { $script:FeatureLabelLookup[$undoAction.FeatureId] } else { $null } if (-not $undoLabel) { $undoLabel = $undoAction.FeatureId } $currentStep++ if ($script:ApplyProgressCallback) { & $script:ApplyProgressCallback $currentStep $totalSteps "Undoing: $undoLabel" } ImportRegistryFile "> Undoing: $undoLabel" (Resolve-UndoRegFilePath $undoAction.UndoRegFile) } foreach ($undoAction in $script:UndoFeatureActions) { if ($script:CancelRequested) { return } $undoLabel = if ($script:FeatureLabelLookup) { $script:FeatureLabelLookup[$undoAction.FeatureId] } else { $null } if (-not $undoLabel) { $undoLabel = $undoAction.FeatureId } $currentStep++ if ($script:ApplyProgressCallback) { & $script:ApplyProgressCallback $currentStep $totalSteps "Undoing: $undoLabel" } Invoke-UndoFeatureAction -FeatureId $undoAction.FeatureId } $script:UndoRegistryKeys = @() $script:UndoFeatureActions = @() if ($script:RegistryImportFailures -gt 0) { Write-Host "" Write-Host "$($script:RegistryImportFailures) registry import change(s) failed. See output above for details." -ForegroundColor Yellow } }