Add comprehensive test suite, fix minor issues, rename function and file names to match approved verbs (#708)

This commit is contained in:
Jeffrey
2026-07-19 22:06:07 +02:00
committed by GitHub
parent a7292e4f35
commit 9c033dbf98
116 changed files with 4629 additions and 650 deletions
@@ -17,7 +17,7 @@
PSCustomObject[] with Name and Id properties. Returns $null on
failure, or an empty array when winget succeeds but lists no apps.
#>
function GetInstalledAppsViaWinget {
function Get-WingetInstalledApps {
param (
[int]$TimeOut = 10,
[switch]$NonBlocking
@@ -107,4 +107,4 @@ function GetInstalledAppsViaWinget {
else {
return & $fetchBlock $TimeOut
}
}
}
@@ -1,6 +1,8 @@
# Forcefully removes Microsoft Edge using its uninstaller
# Credit: Based on work from loadstring1 & ave9858
function ForceRemoveEdge {
<#
.SYNOPSIS
Forcefully uninstalls Microsoft Edge and removes its leftover shortcuts and autostart entries.
#>
function Invoke-ForceRemoveEdge {
Write-Host "> Forcefully uninstalling Microsoft Edge..."
$regView = [Microsoft.Win32.RegistryView]::Registry32
@@ -54,4 +56,4 @@ function ForceRemoveEdge {
else {
Write-Host "Unable to forcefully uninstall Microsoft Edge, uninstaller could not be found" -ForegroundColor Red
}
}
}
@@ -14,12 +14,12 @@
An array of app package identifiers to remove (e.g. 'Microsoft.BingNews').
.EXAMPLE
RemoveApps @('Microsoft.BingNews', 'Microsoft.BingWeather')
Remove-SelectedApps @('Microsoft.BingNews', 'Microsoft.BingWeather')
.EXAMPLE
RemoveApps -appsList (GenerateAppsList)
Remove-SelectedApps -appsList (Generate-AppsList)
#>
function RemoveApps {
function Remove-SelectedApps {
param (
$appslist
)
@@ -33,7 +33,7 @@ function RemoveApps {
return
}
$targetUser = GetTargetUserForAppRemoval
$targetUser = Get-TargetUserForAppRemoval
$appCount = @($appsList).Count
$appIndex = 0
@@ -67,7 +67,7 @@ function RemoveApps {
# Check whether any winget-removed apps are still present, and report errors for each one.
if ($wingetRemovedApps.Count -gt 0) {
$postRemovalList = if ($script:WingetInstalled) { GetInstalledAppsViaWinget -TimeOut 10 -NonBlocking } else { $null }
$postRemovalList = if ($script:WingetInstalled) { Get-WingetInstalledApps -TimeOut 10 -NonBlocking } else { $null }
$edgeForceRemoveRequested = $false
foreach ($app in $wingetRemovedApps) {
@@ -116,7 +116,7 @@ function Remove-WinGetApp {
} -ArgumentList $app
if ($script:Params.ContainsKey("User")) {
Write-Host "Adding scheduled task to uninstall $app for user $(GetUserName)..."
Write-Host "Adding scheduled task to uninstall $app for user $(Get-UserName)..."
Set-RunOnceWingetTask -appId $app
}
elseif ($script:Params.ContainsKey("Sysprep")) {
@@ -185,7 +185,7 @@ function Remove-AppxApp {
The package identifier to check (e.g. 'Microsoft.BingNews').
.PARAMETER InstalledList
Optional pre-fetched array of winget objects from GetInstalledAppsViaWinget.
Optional pre-fetched array of winget objects from Get-WingetInstalledApps.
When provided, used directly; otherwise a live winget call is made.
#>
function Test-AppStillInstalled {
@@ -205,7 +205,7 @@ function Test-AppStillInstalled {
}
if ($script:WingetInstalled) {
$liveList = GetInstalledAppsViaWinget -TimeOut 10 -NonBlocking
$liveList = Get-WingetInstalledApps -TimeOut 10 -NonBlocking
if (Test-AppInWingetList -appId $appId -InstalledList $liveList) {
return $true
}
@@ -241,11 +241,12 @@ function Get-AppRemovalMethod {
foreach ($appData in $appsJson.Apps) {
$rawMethod = $appData.RemovalMethod
$method = if ($rawMethod -and $rawMethod -eq 'WinGet') { 'WinGet' } else { 'Appx' }
if ($appData.AppId -is [array]) {
foreach ($id in $appData.AppId) { $script:AppRemovalMethodCache[$id.Trim()] = $method }
}
else {
$script:AppRemovalMethodCache[$appData.AppId.Trim()] = $method
foreach ($id in @($appData.AppId)) {
if ($id -isnot [string]) { continue }
$normalizedId = $id.Trim()
if (-not [string]::IsNullOrWhiteSpace($normalizedId)) {
$script:AppRemovalMethodCache[$normalizedId] = $method
}
}
}
}
@@ -276,12 +277,12 @@ function Request-EdgeForceRemove {
$result = Show-MessageBox -Message 'Unable to uninstall Microsoft Edge via WinGet. Would you like to forcefully uninstall it? NOT RECOMMENDED!' -Title 'Force Uninstall Microsoft Edge?' -Button 'YesNo' -Icon 'Warning'
if ($result -eq 'Yes') {
Write-Host ""
ForceRemoveEdge
Invoke-ForceRemoveEdge
}
}
elseif ($(Read-Host -Prompt "Would you like to forcefully uninstall Microsoft Edge? NOT RECOMMENDED! (y/n)") -eq 'y') {
Write-Host ""
ForceRemoveEdge
Invoke-ForceRemoveEdge
}
}
+1 -1
View File
@@ -12,7 +12,7 @@
The identifier to search for (e.g. 'Microsoft.Copilot').
.PARAMETER InstalledList
An array of PSCustomObject from GetInstalledAppsViaWinget.
An array of PSCustomObject from Get-WingetInstalledApps.
#>
function Test-AppInWingetList {
param(
@@ -1,6 +1,6 @@
# Shows the CLI app removal menu and prompts the user to select which apps to remove.
function ShowCLIAppRemoval {
PrintHeader "App Removal"
function Show-CliAppRemoval {
Write-CliHeader "App Removal"
Write-Output "> Opening app selection form..."
@@ -8,10 +8,10 @@ function ShowCLIAppRemoval {
if ($result -eq $true) {
Write-Output "You have selected $($script:SelectedApps.Count) apps for removal"
AddParameter 'RemoveApps'
AddParameter 'Apps' ($script:SelectedApps -join ',')
Add-Parameter 'RemoveApps'
Add-Parameter 'Apps' ($script:SelectedApps -join ',')
SaveSettings
Save-Settings
# Suppress prompt if Silent parameter was passed
if (-not $Silent) {
@@ -19,7 +19,7 @@ function ShowCLIAppRemoval {
Write-Output ""
Write-Output "Press enter to remove the selected apps or press CTRL+C to quit..."
Read-Host | Out-Null
PrintHeader "App Removal"
Write-CliHeader "App Removal"
}
}
else {
@@ -1,6 +1,6 @@
# Shows the CLI default mode app removal options. Loops until a valid option is selected.
function ShowCLIDefaultModeAppRemovalOptions {
PrintHeader 'Default Mode'
function Show-CliDefaultModeAppRemovalOptions {
Write-CliHeader 'Default Mode'
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 ""
@@ -1,5 +1,5 @@
# Show CLI default mode options for removing apps, or set selection if RunDefaults or RunDefaultsLite parameter was passed
function ShowCLIDefaultModeOptions {
function Show-CliDefaultModeOptions {
if ($RunDefaults) {
$RemoveAppsInput = '1'
}
@@ -7,7 +7,7 @@ function ShowCLIDefaultModeOptions {
$RemoveAppsInput = '0'
}
else {
$RemoveAppsInput = ShowCLIDefaultModeAppRemovalOptions
$RemoveAppsInput = Show-CliDefaultModeAppRemovalOptions
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') {
@@ -15,40 +15,40 @@ function ShowCLIDefaultModeOptions {
}
}
PrintHeader 'Default Mode'
Write-CliHeader 'Default Mode'
try {
# Select app removal options based on user input
switch ($RemoveAppsInput) {
'1' {
AddParameter 'RemoveApps'
AddParameter 'Apps' 'Default'
Add-Parameter 'RemoveApps'
Add-Parameter 'Apps' 'Default'
}
'2' {
AddParameter 'RemoveApps'
AddParameter 'Apps' ($script:SelectedApps -join ',')
Add-Parameter 'RemoveApps'
Add-Parameter 'Apps' ($script:SelectedApps -join ',')
if ($DisableGameBarIntegrationInput) {
AddParameter 'DisableDVR'
AddParameter 'DisableGameBarIntegration'
Add-Parameter 'DisableDVR'
Add-Parameter 'DisableGameBarIntegration'
}
}
}
LoadSettings -filePath $script:DefaultSettingsFilePath -expectedVersion "1.0"
Import-Settings -filePath $script:DefaultSettingsFilePath -expectedVersion "1.0"
}
catch {
Write-Error "Failed to load settings from DefaultSettings.json file: $_"
AwaitKeyToExit
Wait-ForKeyPress
}
SaveSettings
Save-Settings
if ($Silent) {
# Skip change summary and confirmation prompt
return
}
PrintPendingChanges
PrintHeader 'Default Mode'
Write-PendingChanges
Write-CliHeader 'Default Mode'
}
@@ -1,13 +1,13 @@
# Shows the CLI last used settings from LastUsedSettings.json file, displays pending changes and prompts the user to apply them.
function ShowCLILastUsedSettings {
PrintHeader 'Custom Mode'
function Show-CliLastUsedSettings {
Write-CliHeader 'Custom Mode'
try {
LoadSettings -filePath $script:SavedSettingsFilePath -expectedVersion "1.0"
Import-Settings -filePath $script:SavedSettingsFilePath -expectedVersion "1.0"
}
catch {
Write-Error "Failed to load settings from LastUsedSettings.json file: $_"
AwaitKeyToExit
Wait-ForKeyPress
}
if ($Silent) {
@@ -15,6 +15,6 @@ function ShowCLILastUsedSettings {
return
}
PrintPendingChanges
PrintHeader 'Custom Mode'
Write-PendingChanges
Write-CliHeader 'Custom Mode'
}
@@ -1,9 +1,9 @@
# Shows the CLI menu options and prompts the user to select one. Loops until a valid option is selected.
function ShowCLIMenuOptions {
function Show-CliMenuOptions {
Do {
$ModeSelectionMessage = "Please select an option (1/2)"
PrintHeader 'Menu'
Write-CliHeader 'Menu'
Write-Host "(1) Default mode: Quickly apply the recommended changes"
Write-Host "(2) App removal mode: Select & remove apps, without making other changes"
@@ -1,4 +1,4 @@
function AwaitKeyToExit {
function Wait-ForKeyPress {
# Suppress prompt if Silent parameter was passed
if (-not $Silent) {
Write-Output ""
@@ -8,4 +8,4 @@ function AwaitKeyToExit {
Stop-Transcript
Exit
}
}
@@ -1,5 +1,5 @@
# Prints the header for the script
function PrintHeader {
function Write-CliHeader {
param (
$title
)
@@ -10,11 +10,11 @@ function PrintHeader {
$fullTitle = "$fullTitle (Sysprep mode)"
}
else {
$fullTitle = "$fullTitle (User: $(GetUserName))"
$fullTitle = "$fullTitle (User: $(Get-UserName))"
}
Clear-Host
Write-Host "-------------------------------------------------------------------------------------------"
Write-Host $fullTitle
Write-Host "-------------------------------------------------------------------------------------------"
}
}
@@ -12,7 +12,7 @@
After printing the summary the function pauses until the user presses
Enter, giving them an opportunity to review and cancel via Ctrl+C.
#>
function PrintPendingChanges {
function Write-PendingChanges {
Write-Output "Win11Debloat will make the following changes:"
if ($script:Params['CreateRestorePoint']) {
@@ -32,7 +32,7 @@ function PrintPendingChanges {
continue
}
'RemoveApps' {
$appsList = GenerateAppsList
$appsList = Generate-AppsList
if ($appsList.Count -eq 0) {
Write-Host "No valid apps were selected for removal" -ForegroundColor Yellow
@@ -56,4 +56,4 @@ function PrintPendingChanges {
Write-Output ""
Write-Output "Press enter to execute the script or press CTRL+C to quit..."
Read-Host | Out-Null
}
}
@@ -195,10 +195,18 @@ function Get-RegistryKeySnapshot {
}
}
<#
.SYNOPSIS
Converts an open registry key into a backup snapshot.
.DESCRIPTION
Captures all values or selected value names, records missing selected values,
and recursively captures subkeys when requested.
#>
function Convert-RegistryKeyToSnapshot {
param(
[Parameter(Mandatory)]
[Microsoft.Win32.RegistryKey]$RegistryKey,
$RegistryKey,
[Parameter(Mandatory)]
[string]$FullPath,
[bool]$CaptureAllValues = $false,
@@ -253,20 +261,34 @@ function Convert-RegistryKeyToSnapshot {
}
}
<#
.SYNOPSIS
Converts a registry value into a serializable backup snapshot.
.DESCRIPTION
Preserves the value kind and normalizes supported data types for JSON
serialization without expanding environment-string values. REG_NONE values
are rejected.
#>
function Convert-RegistryValueToSnapshot {
param(
[Parameter(Mandatory)]
[Microsoft.Win32.RegistryKey]$RegistryKey,
$RegistryKey,
[Parameter(Mandatory)]
[AllowEmptyString()]
[string]$ValueName
)
$valueKind = $RegistryKey.GetValueKind($ValueName)
if ($valueKind -eq [Microsoft.Win32.RegistryValueKind]::None) {
throw "REG_NONE registry values are not supported for backup. Key='$($RegistryKey.Name)' Name='$ValueName'"
}
$value = $RegistryKey.GetValue($ValueName, $null, [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)
try {
$normalizedValue = switch ($valueKind) {
([Microsoft.Win32.RegistryValueKind]::Binary) { @($value | ForEach-Object { [int]$_ }) }
# Prevent an empty byte sequence from being unrolled to $null by the switch pipeline.
([Microsoft.Win32.RegistryValueKind]::Binary) { if ($null -eq $value) { ,@() } else { ,@($value | ForEach-Object { [int]$_ }) } }
([Microsoft.Win32.RegistryValueKind]::MultiString) { @($value) }
([Microsoft.Win32.RegistryValueKind]::DWord) { [BitConverter]::ToUInt32([BitConverter]::GetBytes([int32]$value), 0) }
([Microsoft.Win32.RegistryValueKind]::QWord) { [BitConverter]::ToUInt64([BitConverter]::GetBytes([int64]$value), 0) }
@@ -287,12 +309,20 @@ function Convert-RegistryValueToSnapshot {
}
}
<#
.SYNOPSIS
Describes the user profile targeted by a registry backup.
.DESCRIPTION
Returns DefaultUserProfile for Sysprep, User:<name> for an explicit user,
or CurrentUser:<name> otherwise.
#>
function Get-RegistryBackupTargetDescription {
if ($script:Params.ContainsKey('Sysprep')) {
return 'DefaultUserProfile'
}
$resolvedUserName = [string](GetUserName)
$resolvedUserName = [string](Get-UserName)
if ($script:Params.ContainsKey('User')) {
return "User:$resolvedUserName"
@@ -37,7 +37,7 @@ function New-RegistrySettingsBackup {
$backupFilePath = Join-Path $backupDirectory $backupFileName
$backupConfig = Get-RegistryBackupPayload -SelectedFeatures $selectedFeatures -UndoFeatures $undoFeatures -CreatedAt $timestamp
if (-not (SaveToFile -Config $backupConfig -FilePath $backupFilePath -MaxDepth 25)) {
if (-not (Save-ToFile -Config $backupConfig -FilePath $backupFilePath -MaxDepth 25)) {
throw "Failed to save registry backup to '$backupFilePath'"
}
@@ -1,4 +1,4 @@
function CreateSystemRestorePoint {
function Invoke-SystemRestorePoint {
$SysRestore = Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\SystemRestore" -Name "RPSessionInterval"
$failed = $false
@@ -66,7 +66,7 @@ function Test-FeatureApplied {
return (Test-StoreSearchSuggestionsDisabledForAllUsers)
}
$storeDbPath = GetStoreAppsDatabasePathForUser -UserName (GetUserName)
$storeDbPath = Get-StoreAppsDatabasePathForUser -UserName (Get-UserName)
return (Test-StoreSearchSuggestionsDisabled -StoreAppsDatabase $storeDbPath)
}
@@ -1,5 +1,5 @@
# Import & execute regfile
function ImportRegistryFile {
function Import-RegistryFile {
param (
$message,
$path
@@ -4,7 +4,7 @@
.DESCRIPTION
Handles two categories of features:
- Registry-backed: imports the .reg file via ImportRegistryFile, then runs
- Registry-backed: imports the .reg file via Import-RegistryFile, then runs
any post-import side effects (e.g., removing companion app packages).
- Custom logic: app removal, Windows optional features, start menu
replacement, and other special-case features.
@@ -21,17 +21,17 @@ function Invoke-FeatureApply {
# ---- Registry-backed features: import .reg file, then handle side effects ----
if ($feature.RegistryKey) {
ImportRegistryFile "> $applyText..." $feature.RegistryKey
Import-RegistryFile "> $applyText..." $feature.RegistryKey
# Post-import side effects for specific features
switch ($FeatureId) {
'DisableBing' {
# Also remove the app package for Bing search
RemoveApps @('Microsoft.BingSearch')
Remove-SelectedApps @('Microsoft.BingSearch')
}
'DisableCopilot' {
# Also remove the app packages for Copilot
RemoveApps @('Microsoft.Copilot', 'XP9CXNGPPJ97XX')
Remove-SelectedApps @('Microsoft.Copilot', 'XP9CXNGPPJ97XX')
}
'DisableTelemetry' {
# Also disable telemetry scheduled tasks
@@ -44,8 +44,8 @@ function Invoke-FeatureApply {
# ---- Custom features (no registry backing, or special handling required) ----
switch ($FeatureId) {
'RemoveApps' {
Write-Host "> $applyText for $(GetFriendlyTargetUserName)..."
$appsList = GenerateAppsList
Write-Host "> $applyText for $(Get-FriendlyTargetUserName)..."
$appsList = Generate-AppsList
if ($appsList.Count -eq 0) {
Write-Host "No valid apps were selected for removal" -ForegroundColor Yellow
@@ -54,19 +54,19 @@ function Invoke-FeatureApply {
}
Write-Host "$($appsList.Count) apps selected for removal"
RemoveApps $appsList
Remove-SelectedApps $appsList
return
}
'RemoveGamingApps' {
$appsList = @('Microsoft.GamingApp', 'Microsoft.XboxGameOverlay', 'Microsoft.XboxGamingOverlay')
Write-Host "> $applyText..."
RemoveApps $appsList
Remove-SelectedApps $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 "> $applyText..."
RemoveApps $appsList
Remove-SelectedApps $appsList
return
}
'DisableWidgets' {
@@ -76,46 +76,46 @@ function Invoke-FeatureApply {
Get-Process *Widget* -ErrorAction SilentlyContinue | Stop-Process
}
RemoveApps @('Microsoft.StartExperiencesApp','MicrosoftWindows.Client.WebExperience','Microsoft.WidgetsPlatformRuntime')
Remove-SelectedApps @('Microsoft.StartExperiencesApp','MicrosoftWindows.Client.WebExperience','Microsoft.WidgetsPlatformRuntime')
return
}
'EnableWindowsSandbox' {
Write-Host "> $applyText..."
EnableWindowsFeature "Containers-DisposableClientVM"
Enable-WindowsFeature "Containers-DisposableClientVM"
Write-Host ""
return
}
'EnableWindowsSubsystemForLinux' {
Write-Host "> $applyText..."
EnableWindowsFeature "VirtualMachinePlatform"
EnableWindowsFeature "Microsoft-Windows-Subsystem-Linux"
Enable-WindowsFeature "VirtualMachinePlatform"
Enable-WindowsFeature "Microsoft-Windows-Subsystem-Linux"
Write-Host ""
return
}
'ClearStart' {
Write-Host "> $applyText for user $(GetUserName)..."
$startMenuBinFile = GetStartMenuBinPathForUser -UserName (GetUserName)
Write-Host "> $applyText for user $(Get-UserName)..."
$startMenuBinFile = Get-StartMenuBinPathForUser -UserName (Get-UserName)
if (-not [string]::IsNullOrWhiteSpace($startMenuBinFile)) {
ReplaceStartMenu -startMenuBinFile $startMenuBinFile
Replace-StartMenu -startMenuBinFile $startMenuBinFile
}
Write-Host ""
return
}
'ReplaceStart' {
Write-Host "> $applyText for user $(GetUserName)..."
$startMenuBinFile = GetStartMenuBinPathForUser -UserName (GetUserName)
Write-Host "> $applyText for user $(Get-UserName)..."
$startMenuBinFile = Get-StartMenuBinPathForUser -UserName (Get-UserName)
if (-not [string]::IsNullOrWhiteSpace($startMenuBinFile)) {
ReplaceStartMenu -startMenuBinFile $startMenuBinFile -startMenuTemplate $script:Params.Item("ReplaceStart")
Replace-StartMenu -startMenuBinFile $startMenuBinFile -startMenuTemplate $script:Params.Item("ReplaceStart")
}
Write-Host ""
return
}
'ClearStartAllUsers' {
ReplaceStartMenuForAllUsers
Replace-StartMenuForAllUsers
return
}
'ReplaceStartAllUsers' {
ReplaceStartMenuForAllUsers -startMenuTemplate $script:Params.Item("ReplaceStartAllUsers")
Replace-StartMenuForAllUsers -startMenuTemplate $script:Params.Item("ReplaceStartAllUsers")
return
}
'DisableStoreSearchSuggestions' {
@@ -126,10 +126,10 @@ function Invoke-FeatureApply {
return
}
Write-Host "> Disabling Microsoft Store search suggestions for user $(GetUserName)..."
$storeDb = GetStoreAppsDatabasePathForUser -UserName (GetUserName)
Write-Host "> Disabling Microsoft Store search suggestions for user $(Get-UserName)..."
$storeDb = Get-StoreAppsDatabasePathForUser -UserName (Get-UserName)
if ($storeDb) {
DisableStoreSearchSuggestions -StoreAppsDatabase $storeDb
Set-StoreSearchSuggestionsDisabled -StoreAppsDatabase $storeDb
}
Write-Host ""
return
@@ -145,7 +145,7 @@ function Invoke-FeatureApply {
.DESCRIPTION
Handles undo for features that require custom logic rather than a simple
.reg file import. Features with a RegistryUndoKey are handled directly
via ImportRegistryFile in Invoke-UndoFeatures.
via Import-RegistryFile in Invoke-UndoFeatures.
#>
function Invoke-FeatureUndo {
param(
@@ -164,24 +164,24 @@ function Invoke-FeatureUndo {
return
}
Write-Host "> Re-enabling Microsoft Store search suggestions for user $(GetUserName)..."
$storeDb = GetStoreAppsDatabasePathForUser -UserName (GetUserName)
Write-Host "> Re-enabling Microsoft Store search suggestions for user $(Get-UserName)..."
$storeDb = Get-StoreAppsDatabasePathForUser -UserName (Get-UserName)
if ($storeDb) {
EnableStoreSearchSuggestions -StoreAppsDatabase $storeDb
Set-StoreSearchSuggestionsEnabled -StoreAppsDatabase $storeDb
}
Write-Host ""
return
}
'EnableWindowsSandbox' {
Write-Host "> $($feature.ApplyUndoText)..."
DisableWindowsFeature 'Containers-DisposableClientVM'
Disable-WindowsFeature 'Containers-DisposableClientVM'
Write-Host ""
return
}
'EnableWindowsSubsystemForLinux' {
Write-Host "> $($feature.ApplyUndoText)..."
DisableWindowsFeature 'Microsoft-Windows-Subsystem-Linux'
DisableWindowsFeature 'VirtualMachinePlatform'
Disable-WindowsFeature 'Microsoft-Windows-Subsystem-Linux'
Disable-WindowsFeature 'VirtualMachinePlatform'
Write-Host ""
return
}
@@ -287,7 +287,7 @@ function Invoke-UndoFeatures {
}
if ($f -and $f.RegistryUndoKey) {
ImportRegistryFile "> $undoText" (Resolve-UndoRegFilePath $f.RegistryUndoKey)
Import-RegistryFile "> $undoText" (Resolve-UndoRegFilePath $f.RegistryUndoKey)
}
Invoke-FeatureUndo -FeatureId $featureId
@@ -311,8 +311,10 @@ function Invoke-UndoFeatures {
(used by the GUI modal). Cancellation is checked between each step.
#>
function Invoke-AllChanges {
if ($script:CancelRequested) { return }
# Guard: prevent running as SYSTEM account without explicit target user
$isSystem = ([Security.Principal.WindowsIdentity]::GetCurrent().User.Value -eq 'S-1-5-18')
$isSystem = Test-RunningAsSystem
if ($isSystem -and -not $script:Params.ContainsKey("User") -and -not $script:Params.ContainsKey("Sysprep")) {
throw "Win11Debloat is running as the SYSTEM account. Use the '-User' or '-Sysprep' parameter to target a specific user."
}
@@ -355,6 +357,7 @@ function Invoke-AllChanges {
# Phase 1: Registry backup
# ================================================================
if ($needsBackup) {
if ($script:CancelRequested) { return }
$step++
if ($script:ApplyProgressCallback) {
& $script:ApplyProgressCallback $step $totalSteps "Creating registry backup..."
@@ -384,6 +387,7 @@ function Invoke-AllChanges {
# Phase 2: System restore point
# ================================================================
if ($script:Params.ContainsKey("CreateRestorePoint")) {
if ($script:CancelRequested) { return }
$step++
if ($script:ApplyProgressCallback) {
& $script:ApplyProgressCallback $step $totalSteps "Creating system restore point, this may take a moment..."
@@ -394,7 +398,7 @@ function Invoke-AllChanges {
}
else {
Write-Host "> Creating a system restore point..."
CreateSystemRestorePoint
Invoke-SystemRestorePoint
Write-Host ""
}
}
@@ -407,6 +411,8 @@ function Invoke-AllChanges {
$step += $applyIds.Count
}
if ($script:CancelRequested) { return }
# ================================================================
# Phase 4: Undo features
# ================================================================
@@ -423,3 +429,19 @@ function Invoke-AllChanges {
Write-Host "$($script:RegistryImportFailures) registry import change(s) failed. See output above for details." -ForegroundColor Yellow
}
}
<#
.SYNOPSIS
Tests whether Win11Debloat is running under the SYSTEM account.
.DESCRIPTION
Compares the current Windows identity's security identifier (SID) with
the well-known Local System SID (S-1-5-18).
.OUTPUTS
System.Boolean
Returns $true when the current process runs as SYSTEM; otherwise, $false.
#>
function Test-RunningAsSystem {
return ([Security.Principal.WindowsIdentity]::GetCurrent().User.Value -eq 'S-1-5-18')
}
@@ -5,7 +5,7 @@
.DESCRIPTION
Restarts the Explorer process to ensure all UI modifications take effect. Shows a warning if any of the applied features require a reboot to take full effect.
#>
function RestartExplorer {
function Invoke-RestartExplorer {
if ($script:Params.ContainsKey("WhatIf")) {
Write-Host "[WhatIf] Restart the Windows Explorer process" -ForegroundColor Cyan
return
@@ -32,4 +32,4 @@ function RestartExplorer {
else {
Write-Host "Unable to restart Windows Explorer process, please manually reboot your PC to apply all changes" -ForegroundColor Yellow
}
}
}
@@ -248,6 +248,14 @@ function New-RegistryBackupAllowListPlanMap {
return $planMap
}
<#
.SYNOPSIS
Converts registry value names into a case-insensitive set.
.DESCRIPTION
Preserves empty names and prevents PowerShell from enumerating the returned
HashSet.
#>
function ConvertTo-RegistryValueNameSet {
param(
[AllowEmptyCollection()]
@@ -259,9 +267,18 @@ function ConvertTo-RegistryValueNameSet {
$null = $valueNameSet.Add([string]$valueName)
}
return $valueNameSet
# Prevent PowerShell from enumerating the HashSet into an array or single string
return ,$valueNameSet
}
<#
.SYNOPSIS
Validates a registry snapshot against the selected-feature allow list.
.DESCRIPTION
Recursively validates snapshot paths, value names, value kinds, and value
data, appending validation errors to the supplied list.
#>
function Test-RegistrySnapshotAgainstAllowList {
param(
[Parameter(Mandatory)]
@@ -301,7 +318,7 @@ function Test-RegistrySnapshotAgainstAllowList {
$Errors.Add("Backup contains unsupported registry value kind '$kindName' for '$valueReference'.")
}
elseif (-not (Test-RegistryValueDataMatchesKind -KindName $kindName -Data $valueSnapshot.Data)) {
$Errors.Add("Backup value '$valueReference' has Data that does not fit its Kind '$kindName'.")
$Errors.Add("Backup contains invalid registry data for kind '$kindName' at '$valueReference'.")
}
}
elseif (-not [string]::IsNullOrWhiteSpace($kindName)) {
@@ -314,6 +331,64 @@ function Test-RegistrySnapshotAgainstAllowList {
}
}
<#
.SYNOPSIS
Tests whether backed-up registry data is valid for its declared value kind.
.DESCRIPTION
Rejects corrupted or hand-edited backup data that cannot be restored safely,
such as a DWord that overflows UInt32 or binary data containing an invalid byte.
This validation runs before Restore-RegistryKeySnapshot mutates the live
registry, preventing a failed conversion from leaving a partially restored key.
.PARAMETER KindName
The declared registry value kind name, such as DWord, QWord, or Binary.
.PARAMETER Data
The backed-up value data to validate against the declared kind.
.OUTPUTS
System.Boolean
#>
function Test-RegistryValueDataMatchesKind {
param(
[Parameter(Mandatory)]
[string]$KindName,
[AllowNull()]
$Data
)
$kind = [System.Enum]::Parse([Microsoft.Win32.RegistryValueKind], $KindName, $true)
switch ($kind) {
([Microsoft.Win32.RegistryValueKind]::DWord) {
$parsed = [uint32]0
return [uint32]::TryParse([string]$Data, [System.Globalization.NumberStyles]::Integer, [System.Globalization.CultureInfo]::InvariantCulture, [ref]$parsed)
}
([Microsoft.Win32.RegistryValueKind]::QWord) {
$parsed = [uint64]0
return [uint64]::TryParse([string]$Data, [System.Globalization.NumberStyles]::Integer, [System.Globalization.CultureInfo]::InvariantCulture, [ref]$parsed)
}
([Microsoft.Win32.RegistryValueKind]::Binary) {
if ($null -eq $Data -or $Data -isnot [array]) { return $false }
foreach ($item in @($Data)) {
if ($item -isnot [ValueType] -and $item -isnot [string]) { return $false }
$parsed = 0
if (-not [int]::TryParse([string]$item, [ref]$parsed) -or $parsed -lt 0 -or $parsed -gt 255) {
return $false
}
}
return $true
}
([Microsoft.Win32.RegistryValueKind]::MultiString) {
foreach ($item in @($Data)) {
if ($item -isnot [string]) { return $false }
}
return $true
}
default { return ($null -eq $Data -or $Data -is [string]) }
}
}
function Test-RegistryValueAllowedByPlan {
param(
[Parameter(Mandatory)]
@@ -431,6 +506,14 @@ function Get-NormalizedRegistryPathKey {
return "$normalizedHive\\$normalizedSubKey"
}
<#
.SYNOPSIS
Tests whether a registry value-kind name is supported in backups.
.DESCRIPTION
Parses kind names case-insensitively and rejects empty, invalid, Unknown,
and None values.
#>
function Test-RegistryValueKindNameSupported {
param(
[string]$KindName
@@ -442,64 +525,10 @@ function Test-RegistryValueKindNameSupported {
try {
$kind = [System.Enum]::Parse([Microsoft.Win32.RegistryValueKind], $KindName, $true)
return $kind -ne [Microsoft.Win32.RegistryValueKind]::Unknown
return $kind -notin @([Microsoft.Win32.RegistryValueKind]::Unknown, [Microsoft.Win32.RegistryValueKind]::None)
}
catch {
return $false
}
}
<#
.SYNOPSIS
Checks whether a backed-up value's Data can be converted to its declared Kind.
.DESCRIPTION
A corrupted or hand-edited backup can have Data that doesn't fit its declared
Kind (e.g. Kind=DWord with Data=4294967296, which overflows uint32). Restore-
RegistryValueSnapshot's Convert-RegistryValueDataFromBackup performs the same
narrowing casts without a try/catch, and by the time it runs the live registry
subtree has already been deleted (Restore-RegistryKeySnapshot deletes before
rewriting) - so an invalid Data/Kind pairing must be rejected here, before any
restore begins, not left to fail mid-restore. See #686.
.PARAMETER KindName
The value's declared registry kind name (e.g. "DWord", "QWord", "String").
.PARAMETER Data
The value's backed-up data to validate against KindName.
#>
function Test-RegistryValueDataMatchesKind {
param(
[string]$KindName,
$Data
)
$kind = [System.Enum]::Parse([Microsoft.Win32.RegistryValueKind], $KindName, $true)
switch ($kind) {
([Microsoft.Win32.RegistryValueKind]::DWord) {
try {
[void][uint32]$Data
return $true
}
catch {
return $false
}
}
([Microsoft.Win32.RegistryValueKind]::QWord) {
try {
[void][uint64]$Data
return $true
}
catch {
return $false
}
}
default {
# String/MultiString/Binary/None conversions in Convert-RegistryValueDataFromBackup
# cannot throw for arbitrary Data - they stringify, array-map to strings, or fall
# back to an empty byte array / null.
return $true
}
}
}
@@ -14,12 +14,12 @@
bundled with the script (Assets/Start/start2.bin).
.EXAMPLE
ReplaceStartMenuForAllUsers
Replace-StartMenuForAllUsers
.EXAMPLE
ReplaceStartMenuForAllUsers -startMenuTemplate "C:\CustomLayout.bin"
Replace-StartMenuForAllUsers -startMenuTemplate "C:\CustomLayout.bin"
#>
function ReplaceStartMenuForAllUsers {
function Replace-StartMenuForAllUsers {
param (
[string]$startMenuTemplate = "$script:AssetsPath\Start\start2.bin"
)
@@ -34,16 +34,16 @@ function ReplaceStartMenuForAllUsers {
}
# Get path to start menu file for all users
$userPathString = GetUserDirectory -userName "*" -fileName "AppData\Local\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState"
$userPathString = Get-UserDirectory -userName "*" -fileName "AppData\Local\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState"
$usersStartMenuPaths = Get-ChildItem -Path $userPathString -ErrorAction SilentlyContinue
# Go through all users and replace the start menu file
ForEach ($startMenuPath in $usersStartMenuPaths) {
ReplaceStartMenu -startMenuBinFile "$($startMenuPath.Fullname)\start2.bin" -startMenuTemplate $startMenuTemplate
Replace-StartMenu -startMenuBinFile "$($startMenuPath.Fullname)\start2.bin" -startMenuTemplate $startMenuTemplate
}
# 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
$defaultStartMenuPath = Get-UserDirectory -userName "Default" -fileName "AppData\Local\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState" -exitIfPathNotFound $false
if ($script:Params.ContainsKey("WhatIf")) {
Write-Host "[WhatIf] Replace Start Menu for Default user profile with template $startMenuTemplate" -ForegroundColor Cyan
@@ -57,7 +57,7 @@ function ReplaceStartMenuForAllUsers {
}
# Copy template to default profile
ReplaceStartMenu -startMenuBinFile "$($defaultStartMenuPath)\start2.bin" -startMenuTemplate $startMenuTemplate
Replace-StartMenu -startMenuBinFile "$($defaultStartMenuPath)\start2.bin" -startMenuTemplate $startMenuTemplate
Write-Host "Replaced start menu for the default user profile"
Write-Host ""
}
@@ -83,12 +83,12 @@ function ReplaceStartMenuForAllUsers {
bundled with the script (Assets/Start/start2.bin).
.EXAMPLE
ReplaceStartMenu -startMenuBinFile "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin"
Replace-StartMenu -startMenuBinFile "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin"
.EXAMPLE
ReplaceStartMenu -startMenuBinFile "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin" -startMenuTemplate "C:\CustomLayout.bin"
Replace-StartMenu -startMenuBinFile "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin" -startMenuTemplate "C:\CustomLayout.bin"
#>
function ReplaceStartMenu {
function Replace-StartMenu {
param (
[Parameter(Mandatory)]
[string]$startMenuBinFile,
@@ -106,7 +106,7 @@ function ReplaceStartMenu {
return
}
$userName = GetStartMenuUserNameFromPath -StartMenuBinFile $startMenuBinFile
$userName = Get-StartMenuUserNameFromPath -StartMenuBinFile $startMenuBinFile
if ($script:Params.ContainsKey("WhatIf")) {
Write-Host "[WhatIf] Replace Start Menu for user $userName with template $startMenuTemplate" -ForegroundColor Cyan
@@ -147,12 +147,12 @@ function ReplaceStartMenu {
The target username. Pass an empty string or omit to resolve for the current user.
.EXAMPLE
GetStartMenuBinPathForUser -UserName "Jeff"
Get-StartMenuBinPathForUser -UserName "Jeff"
.EXAMPLE
GetStartMenuBinPathForUser -UserName "Default"
Get-StartMenuBinPathForUser -UserName "Default"
#>
function GetStartMenuBinPathForUser {
function Get-StartMenuBinPathForUser {
param(
[string]$UserName
)
@@ -161,7 +161,7 @@ function GetStartMenuBinPathForUser {
return "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin"
}
return (GetUserDirectory -userName $UserName -fileName "AppData\Local\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin" -exitIfPathNotFound $false)
return (Get-UserDirectory -userName $UserName -fileName "AppData\Local\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin" -exitIfPathNotFound $false)
}
<#
@@ -177,9 +177,9 @@ function GetStartMenuBinPathForUser {
The full path to a start2.bin file.
.EXAMPLE
GetStartMenuUserNameFromPath -StartMenuBinFile "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin"
Get-StartMenuUserNameFromPath -StartMenuBinFile "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin"
#>
function GetStartMenuUserNameFromPath {
function Get-StartMenuUserNameFromPath {
param(
[string]$StartMenuBinFile
)
@@ -230,7 +230,7 @@ function Get-StartMenuBackupPath {
return $null
}
else {
$userPathString = GetUserDirectory -userName "*" -fileName "AppData\Local\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState"
$userPathString = Get-UserDirectory -userName "*" -fileName "AppData\Local\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState"
$usersStartMenuPaths = Get-ChildItem -Path $userPathString -ErrorAction SilentlyContinue
foreach ($startMenuPath in $usersStartMenuPaths) {
$latestBackup = Get-ChildItem -Path (Join-Path $startMenuPath.FullName 'Win11Debloat-StartBackup-*.bak') -ErrorAction SilentlyContinue |
@@ -261,19 +261,19 @@ function Get-StartMenuBackupPath {
finds the latest Win11Debloat-StartBackup-*.bak file.
.EXAMPLE
RestoreStartMenuFromBackup -StartMenuBinFile "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin"
Restore-StartMenuFromBackup -StartMenuBinFile "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin"
.EXAMPLE
RestoreStartMenuFromBackup -StartMenuBinFile "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin" -BackupFilePath "C:\Backups\Win11Debloat-StartBackup-20260101_120000.bak"
Restore-StartMenuFromBackup -StartMenuBinFile "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin" -BackupFilePath "C:\Backups\Win11Debloat-StartBackup-20260101_120000.bak"
#>
function RestoreStartMenuFromBackup {
function Restore-StartMenuFromBackup {
param(
[Parameter(Mandatory)]
[string]$StartMenuBinFile,
[string]$BackupFilePath
)
$userName = GetStartMenuUserNameFromPath -StartMenuBinFile $StartMenuBinFile
$userName = Get-StartMenuUserNameFromPath -StartMenuBinFile $StartMenuBinFile
$backupBinFile = if ([string]::IsNullOrWhiteSpace($BackupFilePath)) {
# Auto-detect latest backup in the same folder as the start2.bin
$startMenuDir = Split-Path $StartMenuBinFile -Parent
@@ -342,19 +342,19 @@ function RestoreStartMenuFromBackup {
.DESCRIPTION
Resolves the start2.bin path for the currently logged-in user, then
delegates to RestoreStartMenuFromBackup.
delegates to Restore-StartMenuFromBackup.
.PARAMETER BackupFilePath
Path to the backup file to restore from. If omitted, automatically
finds the latest Win11Debloat-StartBackup-*.bak file.
.EXAMPLE
RestoreStartMenu
Restore-StartMenu
.EXAMPLE
RestoreStartMenu -BackupFilePath "C:\Backups\Win11Debloat-StartBackup-20260101_120000.bak"
Restore-StartMenu -BackupFilePath "C:\Backups\Win11Debloat-StartBackup-20260101_120000.bak"
#>
function RestoreStartMenu {
function Restore-StartMenu {
param(
[string]$BackupFilePath
)
@@ -364,7 +364,7 @@ function RestoreStartMenu {
Write-Host "Restoring start menu for user $targetUserName from backup..."
return RestoreStartMenuFromBackup -StartMenuBinFile $startMenuBinFile -BackupFilePath $BackupFilePath
return Restore-StartMenuFromBackup -StartMenuBinFile $startMenuBinFile -BackupFilePath $BackupFilePath
}
<#
@@ -384,17 +384,17 @@ function RestoreStartMenu {
LocalState folder.
.EXAMPLE
RestoreStartMenuForAllUsers
Restore-StartMenuForAllUsers
.EXAMPLE
RestoreStartMenuForAllUsers -BackupFilePath "C:\Backups\Win11Debloat-StartBackup-20260101_120000.bak"
Restore-StartMenuForAllUsers -BackupFilePath "C:\Backups\Win11Debloat-StartBackup-20260101_120000.bak"
#>
function RestoreStartMenuForAllUsers {
function Restore-StartMenuForAllUsers {
param(
[string]$BackupFilePath
)
$userPathString = GetUserDirectory -userName "*" -fileName "AppData\Local\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState"
$userPathString = Get-UserDirectory -userName "*" -fileName "AppData\Local\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState"
$usersStartMenuPaths = Get-ChildItem -Path $userPathString -ErrorAction SilentlyContinue
$results = @()
@@ -402,10 +402,10 @@ function RestoreStartMenuForAllUsers {
foreach ($startMenuPath in $usersStartMenuPaths) {
$startMenuBinFile = Join-Path $startMenuPath.FullName 'start2.bin'
$results += RestoreStartMenuFromBackup -StartMenuBinFile $startMenuBinFile -BackupFilePath $BackupFilePath
$results += Restore-StartMenuFromBackup -StartMenuBinFile $startMenuBinFile -BackupFilePath $BackupFilePath
}
$defaultStartMenuPath = GetUserDirectory -userName "Default" -fileName "AppData\Local\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState" -exitIfPathNotFound $false
$defaultStartMenuPath = Get-UserDirectory -userName "Default" -fileName "AppData\Local\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState" -exitIfPathNotFound $false
if (Test-Path $defaultStartMenuPath) {
$defaultStartMenuBinFile = Join-Path $defaultStartMenuPath 'start2.bin'
@@ -1,3 +1,16 @@
<#
.SYNOPSIS
Runs a script block against the registry hive for a backup target.
.PARAMETER Target
A supported backup target: DefaultUserProfile or User:<user name>.
.PARAMETER ScriptBlock
The operation to run after the target user hive is available.
.PARAMETER ArgumentObject
Optional object passed to the script block.
#>
function Invoke-WithLoadedRestoreHive {
param(
[Parameter(Mandatory)]
@@ -24,6 +37,13 @@ function Invoke-WithLoadedRestoreHive {
Invoke-WithTargetUserHive -TargetUserName $targetUserName -ScriptBlock $ScriptBlock -ArgumentObject $ArgumentObject
}
<#
.SYNOPSIS
Restores a registry key and its child keys from a backup snapshot.
.PARAMETER Snapshot
The saved registry-key state, including existence, values, and subkeys.
#>
function Restore-RegistryKeySnapshot {
param(
[Parameter(Mandatory)]
@@ -74,10 +94,20 @@ function Restore-RegistryKeySnapshot {
}
}
<#
.SYNOPSIS
Restores or removes a registry value from a backup snapshot.
.PARAMETER RegistryKey
The open registry key that contains the value.
.PARAMETER Snapshot
The saved registry-value state to apply.
#>
function Restore-RegistryValueSnapshot {
param(
[Parameter(Mandatory)]
[Microsoft.Win32.RegistryKey]$RegistryKey,
$RegistryKey,
[Parameter(Mandatory)]
$Snapshot
)
@@ -101,21 +131,20 @@ function Restore-RegistryValueSnapshot {
$RegistryKey.SetValue($valueName, $normalizedData, $valueKind)
}
catch {
$retryBytes = Convert-BackupDataToByteArray -Data $Snapshot.Data
if ($null -ne $retryBytes) {
try {
$RegistryKey.SetValue($valueName, $retryBytes, [Microsoft.Win32.RegistryValueKind]::Binary)
return
}
catch {
# Fall through to original error message for context.
}
}
throw "Failed setting registry value '$valueName' in '$($RegistryKey.Name)': $($_.Exception.Message)"
}
}
<#
.SYNOPSIS
Converts a backed-up registry value-kind name to its .NET enum value.
.PARAMETER KindName
The registry value-kind name stored in the backup.
.OUTPUTS
Microsoft.Win32.RegistryValueKind
#>
function Convert-RegistryValueKindFromBackup {
param(
[string]$KindName
@@ -133,6 +162,16 @@ function Convert-RegistryValueKindFromBackup {
}
}
<#
.SYNOPSIS
Converts backed-up data to a value suitable for registry restoration.
.PARAMETER Kind
The registry value kind that determines how the data is converted.
.PARAMETER Data
The serialized value data from the backup.
#>
function Convert-RegistryValueDataFromBackup {
param(
[Microsoft.Win32.RegistryValueKind]$Kind,
@@ -148,15 +187,20 @@ function Convert-RegistryValueDataFromBackup {
$unsigned = [uint64]$Data
return [BitConverter]::ToInt64([BitConverter]::GetBytes($unsigned), 0)
}
([Microsoft.Win32.RegistryValueKind]::MultiString) { return @($Data | ForEach-Object { [string]$_ }) }
([Microsoft.Win32.RegistryValueKind]::MultiString) { return ,([string[]]@($Data | ForEach-Object { [string]$_ })) }
([Microsoft.Win32.RegistryValueKind]::Binary) {
if ($null -eq $Data) {
return ,(New-Object byte[] 0)
}
$bytes = Convert-BackupDataToByteArray -Data $Data
if ($null -eq $bytes) {
return (New-Object byte[] 0)
throw 'Invalid binary registry data in backup. Expected byte values from 0 through 255.'
}
return $bytes
# Keep the byte array intact instead of writing each byte to the
# pipeline. RegistryKey.SetValue requires a byte[] for Binary.
return ,$bytes
}
([Microsoft.Win32.RegistryValueKind]::None) { return $null }
default {
if ($null -ne $Data) {
return [string]$Data
@@ -167,6 +211,17 @@ function Convert-RegistryValueDataFromBackup {
}
}
<#
.SYNOPSIS
Converts serialized binary backup data to a byte array.
.PARAMETER Data
A byte array or collection of integer byte values from the backup.
.OUTPUTS
System.Byte[]
Returns $null when the input contains invalid byte data.
#>
function Convert-BackupDataToByteArray {
param(
$Data
@@ -207,18 +262,3 @@ function Convert-BackupDataToByteArray {
return ,$bytes
}
function Remove-RegistrySubKeyTreeIfExists {
param(
[Parameter(Mandatory)]
[Microsoft.Win32.RegistryKey]$RootKey,
[Parameter(Mandatory)]
[string]$SubKeyPath
)
$existing = $RootKey.OpenSubKey($SubKeyPath, $false)
if ($existing) {
$existing.Close()
$RootKey.DeleteSubKeyTree($SubKeyPath, $false)
}
}
@@ -12,9 +12,9 @@
.OUTPUTS
PSCustomObject
A normalized registry backup object produced by Normalize-RegistryBackup.
A normalized registry backup object produced by ConvertTo-NormalizedRegistryBackup.
#>
function Load-RegistryBackupFromFile {
function Import-RegistryBackup {
param(
[Parameter(Mandatory)]
[string]$FilePath
@@ -31,7 +31,7 @@ function Load-RegistryBackupFromFile {
throw "Failed to read backup file '$FilePath'. The file is not valid JSON."
}
return Normalize-RegistryBackup -Backup $rawBackup
return ConvertTo-NormalizedRegistryBackup -Backup $rawBackup
}
<#
@@ -52,7 +52,7 @@ function Load-RegistryBackupFromFile {
ComputerName, Target, SelectedFeatures, SelectedUndoFeatures, and
RegistryKeys properties.
#>
function Normalize-RegistryBackup {
function ConvertTo-NormalizedRegistryBackup {
param(
[Parameter(Mandatory)]
$Backup
@@ -93,7 +93,10 @@ function Normalize-RegistryBackup {
}
elseif ($normalizedTarget -like 'CurrentUser:*') {
$targetCurrentUserName = $normalizedTarget.Substring(12)
if ([string]::IsNullOrWhiteSpace($targetCurrentUserName) -or
if (Test-RunningAsSystem) {
$errors.Add("Backup was made for '$targetCurrentUserName' and is user-scoped. Re-run as that user; SYSTEM cannot restore a CurrentUser backup.")
}
elseif ([string]::IsNullOrWhiteSpace($targetCurrentUserName) -or
-not (Test-UserNameMatch -UserNameA $targetCurrentUserName -UserNameB $env:USERNAME)) {
$errors.Add("Backup was made for '$targetCurrentUserName', this does not match current user '$env:USERNAME'.")
}
@@ -176,7 +179,7 @@ function Normalize-RegistryBackup {
registry, loading the appropriate user hive when required.
.PARAMETER Backup
A normalized backup object (as produced by Normalize-RegistryBackup) whose
A normalized backup object (as produced by ConvertTo-NormalizedRegistryBackup) whose
RegistryKeys snapshots should be restored.
.OUTPUTS
@@ -190,7 +193,7 @@ function Restore-RegistryBackupState {
$Backup
)
$friendlyTarget = GetFriendlyRegistryBackupTarget -Target ([string]$Backup.Target)
$friendlyTarget = Get-FriendlyRegistryBackupTarget -Target ([string]$Backup.Target)
if ($script:Params.ContainsKey("WhatIf")) {
Write-Host "[WhatIf] Restore registry backup for $friendlyTarget" -ForegroundColor Cyan
@@ -11,20 +11,20 @@
.EXAMPLE
DisableStoreSearchSuggestionsForAllUsers
#>
function DisableStoreSearchSuggestionsForAllUsers {
function Set-StoreSearchSuggestionsDisabledForAllUsers {
# Get path to Store app database for all users
$userPathString = GetUserDirectory -userName "*" -fileName "AppData\Local\Packages"
$userPathString = Get-UserDirectory -userName "*" -fileName "AppData\Local\Packages"
$usersStoreDbPaths = Get-ChildItem -Path $userPathString -ErrorAction SilentlyContinue
# Go through all users and disable start search suggestions
foreach ($storeDbPath in $usersStoreDbPaths) {
DisableStoreSearchSuggestions -StoreAppsDatabase ($storeDbPath.FullName + "\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db")
Set-StoreSearchSuggestionsDisabled -StoreAppsDatabase ($storeDbPath.FullName + "\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db")
}
# Also disable start search suggestions for the default user profile
$defaultStoreDbPath = GetStoreAppsDatabasePathForUser -UserName "Default"
$defaultStoreDbPath = Get-StoreAppsDatabasePathForUser -UserName "Default"
if ($defaultStoreDbPath) {
DisableStoreSearchSuggestions -StoreAppsDatabase $defaultStoreDbPath
Set-StoreSearchSuggestionsDisabled -StoreAppsDatabase $defaultStoreDbPath
}
}
@@ -45,7 +45,7 @@ function DisableStoreSearchSuggestionsForAllUsers {
.EXAMPLE
DisableStoreSearchSuggestions -StoreAppsDatabase "$env:LOCALAPPDATA\Packages\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db"
#>
function DisableStoreSearchSuggestions {
function Set-StoreSearchSuggestionsDisabled {
param (
[Parameter(Mandatory)]
[string]$StoreAppsDatabase
@@ -95,20 +95,20 @@ function DisableStoreSearchSuggestions {
.EXAMPLE
EnableStoreSearchSuggestionsForAllUsers
#>
function EnableStoreSearchSuggestionsForAllUsers {
function Set-StoreSearchSuggestionsEnabledForAllUsers {
# Get path to Store app database for all users
$userPathString = GetUserDirectory -userName "*" -fileName "AppData\Local\Packages"
$userPathString = Get-UserDirectory -userName "*" -fileName "AppData\Local\Packages"
$usersStoreDbPaths = Get-ChildItem -Path $userPathString -ErrorAction SilentlyContinue
# Go through all users and re-enable start search suggestions
foreach ($storeDbPath in $usersStoreDbPaths) {
EnableStoreSearchSuggestions -StoreAppsDatabase ($storeDbPath.FullName + "\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db")
Set-StoreSearchSuggestionsEnabled -StoreAppsDatabase ($storeDbPath.FullName + "\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db")
}
# Also re-enable for the default user profile
$defaultStoreDbPath = GetStoreAppsDatabasePathForUser -UserName "Default"
$defaultStoreDbPath = Get-StoreAppsDatabasePathForUser -UserName "Default"
if ($defaultStoreDbPath) {
EnableStoreSearchSuggestions -StoreAppsDatabase $defaultStoreDbPath
Set-StoreSearchSuggestionsEnabled -StoreAppsDatabase $defaultStoreDbPath
}
}
@@ -128,7 +128,7 @@ function EnableStoreSearchSuggestionsForAllUsers {
.EXAMPLE
EnableStoreSearchSuggestions -StoreAppsDatabase "$env:LOCALAPPDATA\Packages\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db"
#>
function EnableStoreSearchSuggestions {
function Set-StoreSearchSuggestionsEnabled {
param (
[Parameter(Mandatory)]
[string]$StoreAppsDatabase
@@ -201,12 +201,12 @@ function EnableStoreSearchSuggestions {
The target username. Pass an empty string or omit to resolve for the current user.
.EXAMPLE
GetStoreAppsDatabasePathForUser -UserName "Jeff"
Get-StoreAppsDatabasePathForUser -UserName "Jeff"
.EXAMPLE
GetStoreAppsDatabasePathForUser -UserName "Default"
Get-StoreAppsDatabasePathForUser -UserName "Default"
#>
function GetStoreAppsDatabasePathForUser {
function Get-StoreAppsDatabasePathForUser {
param(
[string]$UserName
)
@@ -215,7 +215,7 @@ function GetStoreAppsDatabasePathForUser {
return "$env:LOCALAPPDATA\Packages\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db"
}
return (GetUserDirectory -userName $UserName -fileName "AppData\Local\Packages\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db" -exitIfPathNotFound $false)
return (Get-UserDirectory -userName $UserName -fileName "AppData\Local\Packages\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db" -exitIfPathNotFound $false)
}
<#
@@ -287,13 +287,13 @@ function Test-StoreSearchSuggestionsDisabled {
function Test-StoreSearchSuggestionsDisabledForAllUsers {
$paths = @()
$userPathString = GetUserDirectory -userName "*" -fileName "AppData\Local\Packages"
$userPathString = Get-UserDirectory -userName "*" -fileName "AppData\Local\Packages"
$usersStoreDbPaths = Get-ChildItem -Path $userPathString -ErrorAction SilentlyContinue
foreach ($storeDbPath in $usersStoreDbPaths) {
$paths += ($storeDbPath.FullName + "\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db")
}
$defaultStoreDbPath = GetStoreAppsDatabasePathForUser -UserName "Default"
$defaultStoreDbPath = Get-StoreAppsDatabasePathForUser -UserName "Default"
if ($defaultStoreDbPath) {
$paths += $defaultStoreDbPath
}
@@ -309,4 +309,4 @@ function Test-StoreSearchSuggestionsDisabledForAllUsers {
}
return $true
}
}
@@ -1,5 +1,5 @@
# Enables a Windows optional feature and pipes its output to the console
function EnableWindowsFeature {
function Enable-WindowsFeature {
param (
[string]$FeatureName
)
@@ -22,7 +22,7 @@ function EnableWindowsFeature {
}
# Disables a Windows optional feature and pipes its output to the console
function DisableWindowsFeature {
function Disable-WindowsFeature {
param (
[string]$FeatureName
)
@@ -1,10 +1,10 @@
# Returns a validated list of apps based on the provided appsList and the supported apps from Apps.json
function ValidateAppslist {
function Get-ValidatedAppList {
param (
$appsList
)
$supportedAppsList = @(LoadAppsDetailsFromJson | ForEach-Object { @($_.AppId) }) | ForEach-Object { $_.Trim() } | Where-Object { $_.Length -gt 0 }
$supportedAppsList = @(Import-AppDetailsFromJson | ForEach-Object { @($_.AppId) }) | ForEach-Object { $_.Trim() } | Where-Object { $_.Length -gt 0 }
$validatedAppsList = @()
# Validate provided appsList against supportedAppsList
@@ -1,5 +1,27 @@
# Read Apps.json and return list of app objects with optional filtering
function LoadAppsDetailsFromJson {
<#
.SYNOPSIS
Loads application details from Apps.json.
.DESCRIPTION
Reads the application definitions from Apps.json, optionally filters the
results to installed applications, and returns normalized app objects for
display and selection.
.PARAMETER OnlyInstalled
Filters the results to applications detected through Appx or the supplied
winget installation list.
.PARAMETER InstalledList
A pre-fetched winget installation list used when filtering installed apps.
.PARAMETER InitialCheckedFromJson
Sets each returned app's IsChecked value from its SelectedByDefault setting.
.OUTPUTS
System.Management.Automation.PSCustomObject[]
Application detail objects containing display, selection, and removal data.
#>
function Import-AppDetailsFromJson {
param (
[switch]$OnlyInstalled,
[object[]]$InstalledList = $null,
@@ -17,8 +39,13 @@ function LoadAppsDetailsFromJson {
foreach ($appData in $jsonContent.Apps) {
# Handle AppId as array (could be single or multiple IDs)
$appIdArray = if ($appData.AppId -is [array]) { $appData.AppId } else { @($appData.AppId) }
$appIdArray = $appIdArray | ForEach-Object { $_.Trim() } | Where-Object { $_.length -gt 0 }
$appIdArray = @(
foreach ($rawAppId in @($appData.AppId)) {
if ($rawAppId -isnot [string]) { continue }
$normalizedAppId = $rawAppId.Trim()
if ($normalizedAppId.Length -gt 0) { $normalizedAppId }
}
)
if ($appIdArray.Count -eq 0) { continue }
if ($OnlyInstalled) {
@@ -1,6 +1,8 @@
# Read Apps.json and return the list of preset objects (Name + AppIds).
# Returns an empty array if the file cannot be read or contains no presets.
function LoadAppPresetsFromJson {
<#
.SYNOPSIS
Returns preset names and application IDs from Apps.json, or an empty array when unavailable.
#>
function Import-AppPresetsFromJson {
try {
$jsonContent = Get-Content -Path $script:AppsListFilePath -Raw | ConvertFrom-Json
}
@@ -14,7 +14,7 @@
System.String[]. An array of app ID strings, or an empty array if the
file does not exist or contains no selected-by-default apps.
#>
function LoadAppsFromFile {
function Import-AppsFromFile {
param (
$appsFilePath
)
@@ -41,6 +41,6 @@ function LoadAppsFromFile {
}
catch {
Write-Error "Unable to read apps list from file: $appsFilePath"
AwaitKeyToExit
Wait-ForKeyPress
}
}
@@ -1,6 +1,8 @@
# 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 {
<#
.SYNOPSIS
Imports a JSON file, optionally validates its version, and returns $null on failure.
#>
function Import-JsonFile {
param (
[string]$filePath,
[string]$expectedVersion = $null,
@@ -1,11 +1,14 @@
# Loads settings from a JSON file and adds them to script params
function LoadSettings {
<#
.SYNOPSIS
Imports enabled, compatible feature settings from a JSON file into the active parameters.
#>
function Import-Settings {
param (
[string]$filePath,
[string]$expectedVersion = "1.0"
)
$settingsJson = LoadJsonFile -filePath $filePath -expectedVersion $expectedVersion
$settingsJson = Import-JsonFile -filePath $filePath -expectedVersion $expectedVersion
if (-not $settingsJson -or -not $settingsJson.Settings) {
throw "Failed to load settings from $(Split-Path $filePath -Leaf)"
@@ -29,6 +32,6 @@ function LoadSettings {
continue
}
AddParameter $setting.Name $setting.Value
Add-Parameter $setting.Name $setting.Value
}
}
@@ -1,5 +1,8 @@
# Saves the current settings, excluding control parameters, to 'LastUsedSettings.json' file
function SaveSettings {
<#
.SYNOPSIS
Saves active feature settings, excluding control parameters, unless running in WhatIf mode.
#>
function Save-Settings {
if ($script:Params.ContainsKey("WhatIf")) {
Write-Host "[WhatIf] Save settings to LastUsedSettings.json" -ForegroundColor Cyan
return
@@ -21,8 +24,8 @@ function SaveSettings {
}
}
if (-not (SaveToFile -Config $settings -FilePath $script:SavedSettingsFilePath)) {
if (-not (Save-ToFile -Config $settings -FilePath $script:SavedSettingsFilePath)) {
Write-Output ""
Write-Host "Error: Failed to save settings to LastUsedSettings.json file" -ForegroundColor Red
}
}
}
+36
View File
@@ -0,0 +1,36 @@
<#
.SYNOPSIS
Serializes a configuration hashtable to a UTF-8 JSON file.
.PARAMETER Config
The configuration data to serialize.
.PARAMETER FilePath
The destination file path.
.PARAMETER MaxDepth
The maximum object depth passed to ConvertTo-Json.
.OUTPUTS
System.Boolean. $true when the file is written; otherwise $false.
#>
function Save-ToFile {
param (
[Parameter(Mandatory=$true)]
[hashtable]$Config,
[Parameter(Mandatory=$true)]
[string]$FilePath,
[Parameter(Mandatory=$false)]
[int]$MaxDepth = 10
)
try {
$Config | ConvertTo-Json -Depth $MaxDepth | Set-Content -Path $FilePath -Encoding UTF8
return $true
}
catch {
return $false
}
}
-22
View File
@@ -1,22 +0,0 @@
# Saves configuration JSON to a file.
# Returns $true on success, $false on failure.
function SaveToFile {
param (
[Parameter(Mandatory=$true)]
[hashtable]$Config,
[Parameter(Mandatory=$true)]
[string]$FilePath,
[Parameter(Mandatory=$false)]
[int]$MaxDepth = 10
)
try {
$Config | ConvertTo-Json -Depth $MaxDepth | Set-Content -Path $FilePath -Encoding UTF8
return $true
}
catch {
return $false
}
}
@@ -1,6 +1,20 @@
# Applies settings from a JSON object to UI controls (checkboxes and comboboxes)
# Used by LoadDefaultsBtn and LoadLastUsedBtn in the UI
function ApplySettingsToUiControls {
<#
.SYNOPSIS
Applies enabled settings from JSON to mapped checkbox and combo-box controls.
.PARAMETER Window
The window that owns the mapped controls.
.PARAMETER SettingsJson
The settings object containing a Settings collection.
.PARAMETER UiControlMappings
The feature-to-control mapping used to locate and update controls.
.OUTPUTS
System.Boolean. $false for invalid settings input; otherwise $true.
#>
function Apply-SettingsToUiControls {
param (
$window,
$settingsJson,
@@ -1,10 +1,20 @@
# 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 {
<#
.SYNOPSIS
Attaches shift-click range-selection behavior to an application checkbox.
.PARAMETER Checkbox
The checkbox that receives the mouse event handler.
.PARAMETER AppsPanel
The panel whose visible checkboxes participate in range selection.
.PARAMETER LastSelectedCheckboxRef
A reference that stores the previously clicked checkbox.
.PARAMETER UpdateStatusCallback
An optional callback invoked after a range selection changes.
#>
function Attach-ShiftClickBehavior {
param (
[System.Windows.Controls.CheckBox]$checkbox,
[System.Windows.Controls.StackPanel]$appsPanel,
@@ -1,5 +1,11 @@
# Checks if the system is set to use dark mode for apps
function GetSystemUsesDarkMode {
<#
.SYNOPSIS
Returns whether Windows apps are configured to use dark mode.
.OUTPUTS
System.Boolean. $false when the personalization setting cannot be read.
#>
function Get-SystemUsesDarkMode {
try {
$personalizeKey = Get-ItemProperty -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize'
+53 -12
View File
@@ -223,7 +223,7 @@ function Update-AppPresetStates {
$script:UpdatingPresets = $true
try {
# Helper: count matching and checked apps, set checkbox state
function SetPresetState($CheckBox, [scriptblock]$MatchFilter) {
function Set-PresetState($CheckBox, [scriptblock]$MatchFilter) {
$total = 0; $checked = 0
foreach ($child in $AppsPanel.Children) {
if ($child -is [System.Windows.Controls.CheckBox]) {
@@ -241,15 +241,15 @@ function Update-AppPresetStates {
$presetDefaultApps = $window.FindName('PresetDefaultApps')
$presetLastUsed = $window.FindName('PresetLastUsed')
SetPresetState $presetDefaultApps { param($c) $c.SelectedByDefault -eq $true }
Set-PresetState $presetDefaultApps { param($c) $c.SelectedByDefault -eq $true }
foreach ($jsonCb in $script:JsonPresetCheckboxes) {
$localIds = $jsonCb.PresetAppIds
SetPresetState $jsonCb { param($c) (@($c.AppIds) | Where-Object { $localIds -contains $_ }).Count -gt 0 }.GetNewClosure()
Set-PresetState $jsonCb { param($c) (@($c.AppIds) | Where-Object { $localIds -contains $_ }).Count -gt 0 }.GetNewClosure()
}
# Last used preset: only update if it's visible (has saved apps)
if ($presetLastUsed.Visibility -ne 'Collapsed' -and $script:SavedAppIds) {
SetPresetState $presetLastUsed { param($c) (@($c.AppIds) | Where-Object { $script:SavedAppIds -contains $_ }).Count -gt 0 }
Set-PresetState $presetLastUsed { param($c) (@($c.AppIds) | Where-Object { $script:SavedAppIds -contains $_ }).Count -gt 0 }
}
}
finally {
@@ -304,7 +304,29 @@ function Find-ParentScrollViewer {
return $null
}
function Load-AppsWithList {
<#
.SYNOPSIS
Loads application details and adds their interactive checkboxes to the main window.
.PARAMETER Window
The main application window and resource owner.
.PARAMETER AppsPanel
The panel populated with application checkboxes.
.PARAMETER OnlyInstalledAppsBox
The filter control that determines whether only installed apps are loaded.
.PARAMETER LoadingAppsIndicator
The loading indicator shown while application details are prepared.
.PARAMETER ImportConfigBtn
The optional import control re-enabled after loading completes.
.PARAMETER ListOfApps
An optional pre-fetched list of installed WinGet applications.
#>
function Add-AppsToMainWindow {
param(
[System.Windows.Window]$Window,
[System.Windows.Controls.Panel]$AppsPanel,
@@ -335,7 +357,7 @@ function Load-AppsWithList {
$script:AppsListFilePath = $appsListFilePath
. $helperScript
. $loaderScript
LoadAppsDetailsFromJson -OnlyInstalled:$onlyInstalled -InstalledList $installedList -InitialCheckedFromJson:$false
Import-AppDetailsFromJson -OnlyInstalled:$onlyInstalled -InstalledList $installedList -InitialCheckedFromJson:$false
} -ArgumentList $loaderScriptPath, $helperScriptPath, $appsFilePath, $ListOfApps, $onlyInstalled
}
@@ -435,7 +457,7 @@ function Load-AppsWithList {
-AppRemovalScopeDescription $w.FindName('AppRemovalScopeDescription') `
-UserSelectionCombo $w.FindName('UserSelectionCombo')
})
AttachShiftClickBehavior -checkbox $checkbox -appsPanel $AppsPanel `
Attach-ShiftClickBehavior -checkbox $checkbox -appsPanel $AppsPanel `
-lastSelectedCheckboxRef ([ref]$script:MainWindowLastSelectedCheckbox) `
-updateStatusCallback {
$w = $script:MainWindow
@@ -449,7 +471,7 @@ function Load-AppsWithList {
$AppsPanel.Children.Add($checkbox) | Out-Null
if (($i + 1) % $batchSize -eq 0) { DoEvents }
if (($i + 1) % $batchSize -eq 0) { Invoke-DoEvents }
}
$sortArrowName = $Window.FindName('SortArrowName')
@@ -480,7 +502,26 @@ function Load-AppsWithList {
}
}
function Load-AppsIntoMainUI {
<#
.SYNOPSIS
Starts asynchronous loading of application checkboxes for the main window.
.PARAMETER Window
The main application window.
.PARAMETER AppsPanel
The panel that receives application checkboxes.
.PARAMETER OnlyInstalledAppsBox
The installed-applications filter control.
.PARAMETER LoadingAppsIndicator
The loading indicator shown until loading completes.
.PARAMETER ImportConfigBtn
The optional import control disabled while loading is in progress.
#>
function Initialize-MainWindowApps {
param(
[System.Windows.Window]$Window,
[System.Windows.Controls.Panel]$AppsPanel,
@@ -511,7 +552,7 @@ function Load-AppsIntoMainUI {
# Force a render so the loading indicator is visible, then schedule the
# actual loading at Background priority so this call returns immediately.
# This is critical when called from Add_Loaded: the window must finish
# its initialization before we start a nested message pump via DoEvents.
# its initialization before we start a nested message pump via Invoke-DoEvents.
$Window.Dispatcher.Invoke([System.Windows.Threading.DispatcherPriority]::Render, [action] {})
$Window.Dispatcher.BeginInvoke([System.Windows.Threading.DispatcherPriority]::Background, [action] {
try {
@@ -519,7 +560,7 @@ function Load-AppsIntoMainUI {
if ($OnlyInstalledAppsBox.IsChecked -and ($script:WingetInstalled -eq $true)) {
Write-Host "Retrieving installed apps via winget..."
$listOfApps = GetInstalledAppsViaWinget -TimeOut 20 -NonBlocking
$listOfApps = Get-WingetInstalledApps -TimeOut 20 -NonBlocking
if ($null -eq $listOfApps) {
Write-Warning "WinGet returned no data (command timed out or failed)"
@@ -528,7 +569,7 @@ function Load-AppsIntoMainUI {
}
}
Load-AppsWithList -Window $Window -AppsPanel $AppsPanel -OnlyInstalledAppsBox $OnlyInstalledAppsBox `
Add-AppsToMainWindow -Window $Window -AppsPanel $AppsPanel -OnlyInstalledAppsBox $OnlyInstalledAppsBox `
-LoadingAppsIndicator $LoadingAppsIndicator -ImportConfigBtn $ImportConfigBtn -ListOfApps $listOfApps
}
catch {
+34 -8
View File
@@ -147,7 +147,20 @@ function Invoke-ShowChangesOverview {
Show-MessageBox -Message $message -Title 'Selected Changes' -Button 'OK' -Icon 'None' -Width 600
}
function Build-TweakPresetControlMap {
<#
.SYNOPSIS
Builds the control values needed to apply a saved tweak preset.
.PARAMETER Window
The window that owns the visible tweak controls.
.PARAMETER SettingsJson
The saved settings object to translate into control values.
.OUTPUTS
System.Collections.Hashtable. Control metadata keyed by control name.
#>
function Get-TweakPresetControlMap {
param(
[System.Windows.Window]$Window,
$SettingsJson
@@ -158,7 +171,7 @@ function Build-TweakPresetControlMap {
return $presetMap
}
# FeatureId -> control metadata, similar to ApplySettingsToUiControls lookup.
# FeatureId -> control metadata, similar to Apply-SettingsToUiControls lookup.
$featureIdIndex = @{}
foreach ($controlName in $script:UiControlMappings.Keys) {
$control = $Window.FindName($controlName)
@@ -199,7 +212,20 @@ function Build-TweakPresetControlMap {
return $presetMap
}
function Build-CategoryTweakPresetMap {
<#
.SYNOPSIS
Builds the enabled state map for visible tweak controls in a category.
.PARAMETER Window
The window that owns the visible tweak controls.
.PARAMETER Category
The category whose mapped controls are included.
.OUTPUTS
System.Collections.Hashtable. Control metadata keyed by control name.
#>
function Get-CategoryTweakPresetMap {
param(
[System.Windows.Window]$Window,
[string]$Category
@@ -375,10 +401,10 @@ function Initialize-TweakPresetSources {
$LastUsedSettingsJson
)
$script:DefaultTweakPresetMap = Build-TweakPresetControlMap -Window $Window -SettingsJson $DefaultSettingsJson
$script:LastUsedTweakPresetMap = Build-TweakPresetControlMap -Window $Window -SettingsJson $LastUsedSettingsJson
$script:PrivacyTweakPresetMap = Build-CategoryTweakPresetMap -Window $Window -Category 'Privacy & Suggested Content'
$script:AITweakPresetMap = Build-CategoryTweakPresetMap -Window $Window -Category 'AI'
$script:DefaultTweakPresetMap = Get-TweakPresetControlMap -Window $Window -SettingsJson $DefaultSettingsJson
$script:LastUsedTweakPresetMap = Get-TweakPresetControlMap -Window $Window -SettingsJson $LastUsedSettingsJson
$script:PrivacyTweakPresetMap = Get-CategoryTweakPresetMap -Window $Window -Category 'Privacy & Suggested Content'
$script:AITweakPresetMap = Get-CategoryTweakPresetMap -Window $Window -Category 'AI'
$presetLastUsedTweaksBtn = $Window.FindName('PresetLastUsedTweaksBtn')
if ($presetLastUsedTweaksBtn) {
@@ -421,7 +447,7 @@ function Update-UserSelectionDescription {
switch ($UserSelectionCombo.SelectedIndex) {
0 {
$currentUserName = GetUserName
$currentUserName = Get-UserName
if ([string]::IsNullOrWhiteSpace($currentUserName)) {
$UserSelectionDescription.Text = "The currently logged-in user profile"
}
+73 -14
View File
@@ -1,13 +1,26 @@
# MainWindow-TweaksBuilder.ps1
# Dynamic tweaks UI construction from Features.json, tweak state management, selection clear, and search/highlight.
function Build-DynamicTweaks {
<#
.SYNOPSIS
Builds the main window's dynamic tweak controls from Features.json.
.PARAMETER Window
The main window whose category columns receive the generated controls.
.PARAMETER WinVersion
The Windows build number used for the category-icon fallback.
.NOTES
Initializes script-scoped control and category mappings used by the tweak UI.
#>
function New-DynamicTweakControls {
param(
[System.Windows.Window]$Window,
[int]$WinVersion
)
$featuresJson = LoadJsonFile -filePath $script:FeaturesFilePath -expectedVersion "1.0"
$featuresJson = Import-JsonFile -filePath $script:FeaturesFilePath -expectedVersion "1.0"
if (-not $featuresJson) {
throw "Unable to load Features.json file. The GUI cannot continue without feature definitions."
@@ -29,7 +42,26 @@ function Build-DynamicTweaks {
$script:TweaksCompactMode = $null
$script:TweaksCardsMovedFromCol2 = @()
function CreateLabeledCombo($parent, $labelText, $comboName, $items) {
<#
.SYNOPSIS
Creates and registers a labeled combo box or a checkbox for a tweak.
.PARAMETER Parent
The panel that receives the generated control.
.PARAMETER LabelText
The display and automation label for the tweak.
.PARAMETER ComboName
The name used to register the generated control.
.PARAMETER Items
The available tweak options; two options produce a checkbox.
.OUTPUTS
System.Windows.Controls.Control. The generated checkbox or combo box.
#>
function New-LabeledCombo($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
@@ -96,7 +128,17 @@ function Build-DynamicTweaks {
return $combo
}
function GetWikiUrlForCategory($category) {
<#
.SYNOPSIS
Returns the Features wiki URL for a tweak category.
.PARAMETER Category
The category name converted to a wiki anchor.
.OUTPUTS
System.String. The category URL, or the Features page for an empty category.
#>
function Get-WikiUrlForCategory($category) {
if (-not $category) { return 'https://github.com/Raphire/Win11Debloat/wiki/Features' }
$slug = $category.ToLowerInvariant()
@@ -107,7 +149,17 @@ function Build-DynamicTweaks {
return "https://github.com/Raphire/Win11Debloat/wiki/Features#$slug"
}
function GetOrCreateCategoryCard($categoryObj) {
<#
.SYNOPSIS
Returns the existing category panel or creates and registers a new one.
.PARAMETER CategoryObj
The category definition containing Name and Icon properties.
.OUTPUTS
System.Windows.Controls.StackPanel. The category's content panel.
#>
function Get-OrCreateCategoryCard($categoryObj) {
$categoryName = $categoryObj.Name
$categoryIcon = $categoryObj.Icon
@@ -152,7 +204,7 @@ function Build-DynamicTweaks {
$helpBtn = New-Object System.Windows.Controls.Button
$helpBtn.Content = $helpIcon
$helpBtn.ToolTip = "Open the wiki for more info on '$categoryName' tweaks"
$helpBtn.Tag = (GetWikiUrlForCategory -category $categoryName)
$helpBtn.Tag = (Get-WikiUrlForCategory -category $categoryName)
$helpBtn.Style = $Window.Resources['CategoryHelpLinkButtonStyle']
$helpBtn.Add_Click({
param($button, $e)
@@ -289,8 +341,8 @@ function Build-DynamicTweaks {
if ($soleFeature.FeatureId -match '^Disable') { $opt = 'Disable' } elseif ($soleFeature.FeatureId -match '^Enable') { $opt = 'Enable' }
$items = @('No Change', $opt)
$comboName = ("Feature_{0}_Combo" -f $soleFeature.FeatureId) -replace '[^a-zA-Z0-9_]', ''
if (-not $panel) { $panel = GetOrCreateCategoryCard -categoryObj $categoryObj }
$combo = CreateLabeledCombo -parent $panel -labelText $soleFeature.Label -comboName $comboName -items $items
if (-not $panel) { $panel = Get-OrCreateCategoryCard -categoryObj $categoryObj }
$combo = New-LabeledCombo -parent $panel -labelText $soleFeature.Label -comboName $comboName -items $items
# attach tooltip from Features.json if present
if ($soleFeature.ToolTip -or $soleFeature.DisableWhenApplied -eq $true) {
$tooltipText = $soleFeature.ToolTip
@@ -314,8 +366,8 @@ function Build-DynamicTweaks {
$items = @('No Change') + ($filteredValues | ForEach-Object { $_.Label })
$comboName = 'Group_{0}Combo' -f $group.GroupId
if (-not $panel) { $panel = GetOrCreateCategoryCard -categoryObj $categoryObj }
$combo = CreateLabeledCombo -parent $panel -labelText $group.Label -comboName $comboName -items $items
if (-not $panel) { $panel = Get-OrCreateCategoryCard -categoryObj $categoryObj }
$combo = New-LabeledCombo -parent $panel -labelText $group.Label -comboName $comboName -items $items
# attach tooltip from UiGroups if present
if ($group.ToolTip) {
$tipBlock = New-Object System.Windows.Controls.TextBlock
@@ -335,8 +387,8 @@ function Build-DynamicTweaks {
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_]', ''
if (-not $panel) { $panel = GetOrCreateCategoryCard -categoryObj $categoryObj }
$combo = CreateLabeledCombo -parent $panel -labelText $feature.Label -comboName $comboName -items $items
if (-not $panel) { $panel = Get-OrCreateCategoryCard -categoryObj $categoryObj }
$combo = New-LabeledCombo -parent $panel -labelText $feature.Label -comboName $comboName -items $items
# attach tooltip from Features.json if present, and include the disabled-state reason
if ($feature.ToolTip -or $feature.DisableWhenApplied -eq $true) {
$tooltipText = $feature.ToolTip
@@ -377,7 +429,7 @@ function Update-CurrentTweakSystemState {
if (-not $script:UiControlMappings) { return }
if (-not $script:Features) { return }
$featuresJson = LoadJsonFile -filePath $script:FeaturesFilePath -expectedVersion "1.0"
$featuresJson = Import-JsonFile -filePath $script:FeaturesFilePath -expectedVersion "1.0"
if (-not $featuresJson) { return }
$groupMap = @{}
@@ -423,7 +475,14 @@ function Update-CurrentTweakSystemState {
}
}
function Load-CurrentTweakStateIntoUI {
<#
.SYNOPSIS
Updates tweak controls to reflect the current system state.
.PARAMETER Window
The window that owns the generated tweak controls.
#>
function Set-CurrentTweakStateInUi {
param([System.Windows.Window]$Window)
Update-CurrentTweakSystemState -Window $Window -ApplyToUi:$true
@@ -17,13 +17,13 @@
When $true, dark theme colors are applied; when $false, light theme colors.
.EXAMPLE
SetWindowThemeResources -window $MainWindow -usesDarkMode $true
Set-WindowThemeResources -window $MainWindow -usesDarkMode $true
.EXAMPLE
SetWindowThemeResources -window $Dialog -usesDarkMode $false
Set-WindowThemeResources -window $Dialog -usesDarkMode $false
#>
# Sets resource colors for a WPF window based on dark mode preference
function SetWindowThemeResources {
function Set-WindowThemeResources {
param (
$window,
[bool]$usesDarkMode
+10 -3
View File
@@ -1,3 +1,10 @@
<#
.SYNOPSIS
Displays the themed About dialog for the application.
.PARAMETER Owner
The optional window that owns the dialog and its modal overlay.
#>
function Show-AboutDialog {
param (
[Parameter(Mandatory=$false)]
@@ -6,7 +13,7 @@ function Show-AboutDialog {
Add-Type -AssemblyName PresentationFramework,PresentationCore,WindowsBase | Out-Null
$usesDarkMode = GetSystemUsesDarkMode
$usesDarkMode = Get-SystemUsesDarkMode
# Determine owner window
$ownerWindow = if ($Owner) { $Owner } else { $script:GuiWindow }
@@ -42,7 +49,7 @@ function Show-AboutDialog {
}
# Apply theme resources
SetWindowThemeResources -window $aboutWindow -usesDarkMode $usesDarkMode
Set-WindowThemeResources -window $aboutWindow -usesDarkMode $usesDarkMode
# Get UI elements
$titleBar = $aboutWindow.FindName('TitleBar')
@@ -95,4 +102,4 @@ function Show-AboutDialog {
catch { }
}
}
}
}
+24 -12
View File
@@ -1,8 +1,14 @@
# Shows application selection window that allows the user to select what apps they want to remove or keep
<#
.SYNOPSIS
Displays the application-selection dialog and records the confirmed selections.
.OUTPUTS
System.Nullable[System.Boolean]. The dialog result; confirmed application IDs are stored in $script:SelectedApps.
#>
function Show-AppSelectionWindow {
Add-Type -AssemblyName PresentationFramework,PresentationCore,WindowsBase | Out-Null
$usesDarkMode = GetSystemUsesDarkMode
$usesDarkMode = Get-SystemUsesDarkMode
# Show overlay if main window exists
$overlay = $null
@@ -34,7 +40,7 @@ function Show-AppSelectionWindow {
catch { }
}
SetWindowThemeResources -window $window -usesDarkMode $usesDarkMode
Set-WindowThemeResources -window $window -usesDarkMode $usesDarkMode
$appsPanel = $window.FindName('AppsPanel')
$checkAllBox = $window.FindName('CheckAllBox')
@@ -46,8 +52,14 @@ function Show-AppSelectionWindow {
# Track the last selected checkbox for shift-click range selection
$script:AppSelectionWindowLastSelectedCheckbox = $null
# Loads apps into the apps UI
function LoadApps {
<#
.SYNOPSIS
Reloads the application-selection checkboxes using the current installed-apps filter.
.NOTES
Updates the dialog loading indicator and resets range-selection state.
#>
function Load-Apps {
# Show loading indicator
$loadingIndicator.Visibility = 'Visible'
$window.Dispatcher.Invoke([System.Windows.Threading.DispatcherPriority]::Background, [action]{})
@@ -57,7 +69,7 @@ function Show-AppSelectionWindow {
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 -NonBlocking
$listOfApps = Get-WingetInstalledApps -TimeOut 10 -NonBlocking
if ($null -eq $listOfApps) {
# Show error that the script was unable to get list of apps from WinGet
Show-MessageBox -Message 'Unable to load list of installed apps via WinGet.' -Title 'Error' -Button 'OK' -Icon 'Error' -Owner $window | Out-Null
@@ -65,7 +77,7 @@ function Show-AppSelectionWindow {
}
}
$appsToAdd = LoadAppsDetailsFromJson -OnlyInstalled:$onlyInstalledBox.IsChecked -InstalledList $listOfApps -InitialCheckedFromJson:$true
$appsToAdd = Import-AppDetailsFromJson -OnlyInstalled:$onlyInstalledBox.IsChecked -InstalledList $listOfApps -InitialCheckedFromJson:$true
# Reset the last selected checkbox when loading a new list
$script:AppSelectionWindowLastSelectedCheckbox = $null
@@ -82,7 +94,7 @@ function Show-AppSelectionWindow {
$checkbox.Style = $window.Resources["AppsPanelCheckBoxStyle"]
# Attach shift-click behavior for range selection
AttachShiftClickBehavior -checkbox $checkbox -appsPanel $appsPanel -lastSelectedCheckboxRef ([ref]$script:AppSelectionWindowLastSelectedCheckbox)
Attach-ShiftClickBehavior -checkbox $checkbox -appsPanel $appsPanel -lastSelectedCheckboxRef ([ref]$script:AppSelectionWindowLastSelectedCheckbox)
$appsPanel.Children.Add($checkbox) | Out-Null
}
@@ -112,8 +124,8 @@ function Show-AppSelectionWindow {
}
})
$onlyInstalledBox.Add_Checked({ LoadApps })
$onlyInstalledBox.Add_Unchecked({ LoadApps })
$onlyInstalledBox.Add_Checked({ Load-Apps })
$onlyInstalledBox.Add_Unchecked({ Load-Apps })
$confirmBtn.Add_Click({
$selectedApps = @()
@@ -130,7 +142,7 @@ function Show-AppSelectionWindow {
return
}
if (-not (ConfirmUnsafeAppRemoval -SelectedApps $selectedApps -Owner $window)) {
if (-not (Confirm-UnsafeAppRemoval -SelectedApps $selectedApps -Owner $window)) {
return
}
@@ -141,7 +153,7 @@ function Show-AppSelectionWindow {
# Load apps after window is shown (allows UI to render first)
$window.Add_ContentRendered({
$window.Dispatcher.Invoke([System.Windows.Threading.DispatcherPriority]::Background, [action]{ LoadApps }) | Out-Null
$window.Dispatcher.Invoke([System.Windows.Threading.DispatcherPriority]::Background, [action]{ Load-Apps }) | Out-Null
})
# Show the window and return dialog result
+18 -8
View File
@@ -1,14 +1,24 @@
<#
.SYNOPSIS
Displays the modal progress window while selected changes are applied.
.PARAMETER Owner
The optional window that owns the modal and its overlay.
.PARAMETER InvokeRestartExplorer
Indicates whether the modal should run the Explorer-restart flow after applying changes.
#>
function Show-ApplyModal {
param (
[Parameter(Mandatory=$false)]
[System.Windows.Window]$Owner = $null,
[Parameter(Mandatory=$false)]
[bool]$RestartExplorer = $false
[bool]$InvokeRestartExplorer = $false
)
Add-Type -AssemblyName PresentationFramework,PresentationCore,WindowsBase | Out-Null
$usesDarkMode = GetSystemUsesDarkMode
$usesDarkMode = Get-SystemUsesDarkMode
# Determine owner window
$ownerWindow = if ($Owner) { $Owner } else { $script:GuiWindow }
@@ -44,7 +54,7 @@ function Show-ApplyModal {
}
# Apply theme resources
SetWindowThemeResources -window $applyWindow -usesDarkMode $usesDarkMode
Set-WindowThemeResources -window $applyWindow -usesDarkMode $usesDarkMode
# Get UI elements
$script:ApplyInProgressPanel = $applyWindow.FindName('ApplyInProgressPanel')
@@ -81,7 +91,7 @@ function Show-ApplyModal {
$pct = if ($totalSteps -gt 0) { [math]::Round((($currentStep - 1) / $totalSteps) * 100) } else { 0 }
$script:ApplyProgressBarEl.Value = $pct
# Process pending window messages to keep UI responsive
DoEvents
Invoke-DoEvents
}
# Sub-step callback updates step name and interpolates progress bar within the current step
@@ -96,7 +106,7 @@ function Show-ApplyModal {
$stepFraction = ($subIndex / $subCount) / $totalSteps
$script:ApplyProgressBarEl.Value = [math]::Round(($baseProgress + $stepFraction) * 100)
}
DoEvents
Invoke-DoEvents
}
# Run changes in background to keep UI responsive
@@ -107,8 +117,8 @@ function Show-ApplyModal {
$registryImportFailureCount = [int]$script:RegistryImportFailures
# Restart explorer if requested
if ($RestartExplorer -and -not $script:CancelRequested) {
RestartExplorer
if ($InvokeRestartExplorer -and -not $script:CancelRequested) {
Invoke-RestartExplorer
# Wait for Explorer to finish relaunching, then reclaim focus.
Start-Sleep -Milliseconds 800
@@ -143,7 +153,7 @@ function Show-ApplyModal {
$script:ApplyCompletionTitleEl.Text = "Changes Applied"
# Show completion message with reboot instructions if any applied features require reboot
if ($RestartExplorer) {
if ($InvokeRestartExplorer) {
$rebootFeatures = Get-RebootFeatureLabels
if ($rebootFeatures.Count -gt 0) {
@@ -1,3 +1,7 @@
<#
.SYNOPSIS
Shows a modal category-selection dialog for importing or exporting configuration.
#>
function Show-ImportExportConfigWindow {
param (
[System.Windows.Window]$Owner,
@@ -45,7 +49,7 @@ function Show-ImportExportConfigWindow {
}
$dlg.Owner = $Owner
SetWindowThemeResources -window $dlg -usesDarkMode $UsesDarkMode
Set-WindowThemeResources -window $dlg -usesDarkMode $UsesDarkMode
# Copy the CheckBox default style from the main window so checkboxes get the themed template
try {
@@ -308,7 +312,11 @@ function Build-CategoryDetails {
return $details
}
function Apply-ImportedApplications {
<#
.SYNOPSIS
Applies imported application selections to the application checkboxes.
#>
function Set-ImportedApplications {
param (
[System.Windows.Controls.Panel]$AppsPanel,
[string[]]$AppIds
@@ -321,7 +329,11 @@ function Apply-ImportedApplications {
}
}
function Apply-ImportedTweakSettings {
<#
.SYNOPSIS
Applies imported tweak settings to their mapped UI controls.
#>
function Set-ImportedTweakSettings {
param (
[System.Windows.Window]$Owner,
[hashtable]$UiControlMappings,
@@ -329,10 +341,14 @@ function Apply-ImportedTweakSettings {
)
$settingsJson = [PSCustomObject]@{ Settings = @($TweakSettings) }
ApplySettingsToUiControls -window $Owner -settingsJson $settingsJson -uiControlMappings $UiControlMappings
Apply-SettingsToUiControls -window $Owner -settingsJson $settingsJson -uiControlMappings $UiControlMappings
}
function Apply-ImportedDeploymentSettings {
<#
.SYNOPSIS
Applies imported deployment settings to the deployment controls.
#>
function Set-ImportedDeploymentSettings {
param (
[System.Windows.Window]$Owner,
[System.Windows.Controls.ComboBox]$UserSelectionCombo,
@@ -368,6 +384,10 @@ function Apply-ImportedDeploymentSettings {
}
}
<#
.SYNOPSIS
Exports selected application, tweak, and deployment settings to a configuration file.
#>
function Export-Configuration {
param (
[System.Windows.Window]$Owner,
@@ -427,7 +447,7 @@ function Export-Configuration {
return
}
if (SaveToFile -Config $config -FilePath $saveDialog.FileName) {
if (Save-ToFile -Config $config -FilePath $saveDialog.FileName) {
Write-Host "Configuration exported successfully: $($saveDialog.FileName)"
Show-MessageBox -Message "Configuration exported successfully." -Title 'Export Configuration' -Button 'OK' -Icon 'Information' | Out-Null
}
@@ -437,6 +457,10 @@ function Export-Configuration {
}
}
<#
.SYNOPSIS
Imports selected application, tweak, and deployment settings from a configuration file.
#>
function Import-Configuration {
param (
[System.Windows.Window]$Owner,
@@ -462,7 +486,7 @@ function Import-Configuration {
Write-Host "Importing configuration from '$($openDialog.FileName)'..."
$config = LoadJsonFile -filePath $openDialog.FileName -expectedVersion '1.0'
$config = Import-JsonFile -filePath $openDialog.FileName -expectedVersion '1.0'
if (-not $config) {
Write-Error "Failed to read configuration file '$($openDialog.FileName)'"
Show-MessageBox -Message "Failed to read configuration file" -Title 'Invalid Config' -Button 'OK' -Icon 'Error' | Out-Null
@@ -504,7 +528,7 @@ function Import-Configuration {
)
Write-Host "Importing $($appIds.Count) app selection(s)."
Apply-ImportedApplications -AppsPanel $AppsPanel -AppIds $appIds
Set-ImportedApplications -AppsPanel $AppsPanel -AppIds $appIds
if ($OnAppsImported) {
& $OnAppsImported
@@ -513,11 +537,11 @@ function Import-Configuration {
if ($categories -contains 'System Tweaks' -and $config.Tweaks) {
$tweakCount = @($config.Tweaks).Count
Write-Host "Importing $tweakCount tweak(s)."
Apply-ImportedTweakSettings -Owner $Owner -UiControlMappings $UiControlMappings -TweakSettings @($config.Tweaks)
Set-ImportedTweakSettings -Owner $Owner -UiControlMappings $UiControlMappings -TweakSettings @($config.Tweaks)
}
if ($categories -contains 'Deployment Settings' -and $config.Deployment) {
Write-Host 'Importing deployment settings.'
Apply-ImportedDeploymentSettings -Owner $Owner -UserSelectionCombo $UserSelectionCombo -OtherUsernameTextBox $OtherUsernameTextBox -DeploymentSettings @($config.Deployment)
Set-ImportedDeploymentSettings -Owner $Owner -UserSelectionCombo $UserSelectionCombo -OtherUsernameTextBox $OtherUsernameTextBox -DeploymentSettings @($config.Deployment)
}
Write-Host 'Configuration imported successfully.'
+34 -30
View File
@@ -1,8 +1,12 @@
function Show-MainWindow {
<#
.SYNOPSIS
Creates and displays the main Win11Debloat window.
#>
function Show-MainWindow {
Add-Type -AssemblyName PresentationFramework,PresentationCore,WindowsBase,System.Windows.Forms | Out-Null
$WinVersion = Get-ItemPropertyValue 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' CurrentBuild
$usesDarkMode = GetSystemUsesDarkMode
$usesDarkMode = Get-SystemUsesDarkMode
# ---- Load XAML ----
$xaml = Get-Content -Path $script:MainWindowSchema -Raw
@@ -14,7 +18,7 @@
$reader.Close()
}
SetWindowThemeResources -window $window -usesDarkMode $usesDarkMode
Set-WindowThemeResources -window $window -usesDarkMode $usesDarkMode
$mainBorder = $window.FindName('MainBorder')
$titleBarBackground = $window.FindName('TitleBarBackground')
@@ -223,7 +227,7 @@
if ($importConfigBtn) { $importConfigBtn.IsEnabled = $false }
# ---- Build JSON-defined app presets ----
foreach ($preset in (LoadAppPresetsFromJson)) {
foreach ($preset in (Import-AppPresetsFromJson)) {
$checkbox = New-Object System.Windows.Controls.CheckBox
$checkbox.Content = $preset.Name
$checkbox.IsThreeState = $true
@@ -301,8 +305,8 @@
# ---- Load apps ----
$appLoadStatusCallback = { Update-AppSelectionStatus -AppsPanel $appsPanel -AppSelectionStatus $appSelectionStatus -AppRemovalScopeCombo $appRemovalScopeCombo -AppRemovalScopeSection $appRemovalScopeSection -AppRemovalScopeDescription $appRemovalScopeDescription -UserSelectionCombo $userSelectionCombo }
$onlyInstalledAppsBox.Add_Checked({ Load-AppsIntoMainUI -Window $window -AppsPanel $appsPanel -OnlyInstalledAppsBox $onlyInstalledAppsBox -LoadingAppsIndicator $loadingAppsIndicator -ImportConfigBtn $importConfigBtn })
$onlyInstalledAppsBox.Add_Unchecked({ Load-AppsIntoMainUI -Window $window -AppsPanel $appsPanel -OnlyInstalledAppsBox $onlyInstalledAppsBox -LoadingAppsIndicator $loadingAppsIndicator -ImportConfigBtn $importConfigBtn })
$onlyInstalledAppsBox.Add_Checked({ Initialize-MainWindowApps -Window $window -AppsPanel $appsPanel -OnlyInstalledAppsBox $onlyInstalledAppsBox -LoadingAppsIndicator $loadingAppsIndicator -ImportConfigBtn $importConfigBtn })
$onlyInstalledAppsBox.Add_Unchecked({ Initialize-MainWindowApps -Window $window -AppsPanel $appsPanel -OnlyInstalledAppsBox $onlyInstalledAppsBox -LoadingAppsIndicator $loadingAppsIndicator -ImportConfigBtn $importConfigBtn })
# ---- App presets popup ----
$presetsPopup.Add_Opened({
@@ -617,9 +621,9 @@
$ShowCurrentlyAppliedTweaksCheckBox.IsChecked = $false
}
$defaultsJson = LoadJsonFile -filePath $script:DefaultSettingsFilePath -expectedVersion "1.0"
$defaultsJson = Import-JsonFile -filePath $script:DefaultSettingsFilePath -expectedVersion "1.0"
if ($defaultsJson) {
ApplySettingsToUiControls -window $window -settingsJson $defaultsJson -uiControlMappings $script:UiControlMappings
Apply-SettingsToUiControls -window $window -settingsJson $defaultsJson -uiControlMappings $script:UiControlMappings
}
if ($script:IsLoadingApps) {
@@ -663,17 +667,17 @@
$hasAppSelection = ($selectedApps.Count -gt 0)
if ($selectedApps.Count -gt 0) {
if (-not (ConfirmUnsafeAppRemoval -SelectedApps $selectedApps -Owner $window)) { return }
if (-not (Confirm-UnsafeAppRemoval -SelectedApps $selectedApps -Owner $window)) { return }
AddParameter 'RemoveApps'
AddParameter 'Apps' ($selectedApps -join ',')
Add-Parameter 'RemoveApps'
Add-Parameter 'Apps' ($selectedApps -join ',')
$selectedScopeItem = $appRemovalScopeCombo.SelectedItem
if ($selectedScopeItem) {
switch ($selectedScopeItem.Content) {
"All users" { AddParameter 'AppRemovalTarget' 'AllUsers' }
"Current user only" { AddParameter 'AppRemovalTarget' 'CurrentUser' }
"Target user only" { AddParameter 'AppRemovalTarget' ($otherUsernameTextBox.Text.Trim()) }
"All users" { Add-Parameter 'AppRemovalTarget' 'AllUsers' }
"Current user only" { Add-Parameter 'AppRemovalTarget' 'CurrentUser' }
"Target user only" { Add-Parameter 'AppRemovalTarget' ($otherUsernameTextBox.Text.Trim()) }
}
}
}
@@ -681,7 +685,7 @@
# Apply dynamic tweaks
foreach ($tweakAction in @(Get-PendingTweakActions -Window $window -ShowAppliedTweaksMode:$showAppliedTweaksMode)) {
if ($tweakAction.Action -eq 'Apply') {
AddParameter $tweakAction.FeatureId
Add-Parameter $tweakAction.FeatureId
$null = $selectedForwardFeatureIds.Add([string]$tweakAction.FeatureId)
continue
}
@@ -695,27 +699,27 @@
$restorePointCheckBox = $window.FindName('RestorePointCheckBox')
if ($restorePointCheckBox -and $restorePointCheckBox.IsChecked) {
AddParameter 'CreateRestorePoint'
Add-Parameter 'CreateRestorePoint'
}
switch ($userSelectionCombo.SelectedIndex) {
0 { Write-Host "Selected user mode: current user ($(GetUserName))" }
0 { Write-Host "Selected user mode: current user ($(Get-UserName))" }
1 {
Write-Host "Selected user mode: $($otherUsernameTextBox.Text.Trim())"
AddParameter User ($otherUsernameTextBox.Text.Trim())
Add-Parameter User ($otherUsernameTextBox.Text.Trim())
}
2 {
Write-Host "Selected user mode: default user profile (Sysprep)"
AddParameter Sysprep
Add-Parameter Sysprep
}
}
SaveSettings
Save-Settings
$restartExplorerCheckBox = $window.FindName('RestartExplorerCheckBox')
$shouldRestartExplorer = $restartExplorerCheckBox -and $restartExplorerCheckBox.IsChecked
Show-ApplyModal -Owner $window -RestartExplorer $shouldRestartExplorer
Show-ApplyModal -Owner $window -InvokeRestartExplorer $shouldRestartExplorer
$window.Close()
})
@@ -737,12 +741,12 @@
$window.Add_Loaded({
try {
& $updateHomeContentPosition
Build-DynamicTweaks -Window $window -WinVersion $WinVersion
Load-CurrentTweakStateIntoUI -Window $window
New-DynamicTweakControls -Window $window -WinVersion $WinVersion
Set-CurrentTweakStateInUi -Window $window
Update-TweaksResponsiveColumns -Window $window
$lastUsedSettingsJson = LoadJsonFile -filePath $script:SavedSettingsFilePath -expectedVersion "1.0" -optionalFile
$defaultsJson = LoadJsonFile -filePath $script:DefaultSettingsFilePath -expectedVersion "1.0"
$lastUsedSettingsJson = Import-JsonFile -filePath $script:SavedSettingsFilePath -expectedVersion "1.0" -optionalFile
$defaultsJson = Import-JsonFile -filePath $script:DefaultSettingsFilePath -expectedVersion "1.0"
$script:SavedAppIds = Get-SavedAppIdsFromSettingsJson -SettingsJson $lastUsedSettingsJson
@@ -750,13 +754,13 @@
Register-TweakPresetControlStateHandlers -Window $window
Update-TweakPresetStates -Window $window
Load-AppsIntoMainUI -Window $window -AppsPanel $appsPanel -OnlyInstalledAppsBox $onlyInstalledAppsBox -LoadingAppsIndicator $loadingAppsIndicator -ImportConfigBtn $importConfigBtn
Initialize-MainWindowApps -Window $window -AppsPanel $appsPanel -OnlyInstalledAppsBox $onlyInstalledAppsBox -LoadingAppsIndicator $loadingAppsIndicator -ImportConfigBtn $importConfigBtn
# Update Current User label
if ($userSelectionCombo -and $userSelectionCombo.Items.Count -gt 0) {
$currentUserItem = $userSelectionCombo.Items[0]
if ($currentUserItem -is [System.Windows.Controls.ComboBoxItem]) {
$currentUserItem.Content = "Current User ($(GetUserName))"
$currentUserItem.Content = "Current User ($(Get-UserName))"
}
}
@@ -809,8 +813,8 @@
})
# ---- Tweak presets wiring ----
$lastUsedSettingsJson = LoadJsonFile -filePath $script:SavedSettingsFilePath -expectedVersion "1.0" -optionalFile
$defaultsJson = LoadJsonFile -filePath $script:DefaultSettingsFilePath -expectedVersion "1.0"
$lastUsedSettingsJson = Import-JsonFile -filePath $script:SavedSettingsFilePath -expectedVersion "1.0" -optionalFile
$defaultsJson = Import-JsonFile -filePath $script:DefaultSettingsFilePath -expectedVersion "1.0"
$script:DefaultTweakPresetMap = @{}
$script:LastUsedTweakPresetMap = @{}
$script:PrivacyTweakPresetMap = @{}
@@ -869,7 +873,7 @@
# ---- Preload app data ----
try {
$script:PreloadedAppData = LoadAppsDetailsFromJson -OnlyInstalled:$false -InstalledList $null -InitialCheckedFromJson:$false
$script:PreloadedAppData = Import-AppDetailsFromJson -OnlyInstalled:$false -InstalledList $null -InitialCheckedFromJson:$false
}
catch {
Write-Warning "Failed to preload apps list: $_"
+6 -3
View File
@@ -1,4 +1,7 @@
# Shows a Windows 11 styled custom message box
<#
.SYNOPSIS
Shows a themed Windows 11-style message box.
#>
function Show-MessageBox {
param (
[Parameter(Mandatory=$true)]
@@ -24,7 +27,7 @@ function Show-MessageBox {
Add-Type -AssemblyName PresentationFramework,PresentationCore,WindowsBase | Out-Null
$usesDarkMode = GetSystemUsesDarkMode
$usesDarkMode = Get-SystemUsesDarkMode
# Determine owner window - use provided Owner, or fall back to main GUI window
$ownerWindow = if ($Owner) { $Owner } else { $script:GuiWindow }
@@ -69,7 +72,7 @@ function Show-MessageBox {
}
# Apply theme resources
SetWindowThemeResources -window $msgWindow -usesDarkMode $usesDarkMode
Set-WindowThemeResources -window $msgWindow -usesDarkMode $usesDarkMode
# Get UI elements
$titleText = $msgWindow.FindName('TitleText')
+7 -7
View File
@@ -15,7 +15,7 @@
Hashtable
Returns a Hashtable describing the user's choice. Possible shapes:
RestoreRegistry - @{ Result='RestoreRegistry'; Backup=<normalizedBackup> }
RestoreStartMenu - @{ Result='RestoreStartMenu'; StartMenuScope=<scope>;
Restore-StartMenu - @{ Result='Restore-StartMenu'; StartMenuScope=<scope>;
UseManualBackupFile=<bool>; BackupFilePath=<path|string> }
Cancelled - @{ Result='Cancelled' } (from New-RestoreDialogState)
#>
@@ -26,7 +26,7 @@ function Show-RestoreBackupDialog {
Add-Type -AssemblyName PresentationFramework,PresentationCore,WindowsBase | Out-Null
$usesDarkMode = GetSystemUsesDarkMode
$usesDarkMode = Get-SystemUsesDarkMode
$ownerWindow = if ($Owner) { $Owner } else { $script:GuiWindow }
$overlay = $null
@@ -67,7 +67,7 @@ function Show-RestoreBackupDialog {
}
try {
SetWindowThemeResources -window $window -usesDarkMode $usesDarkMode
Set-WindowThemeResources -window $window -usesDarkMode $usesDarkMode
}
catch { }
@@ -129,7 +129,7 @@ function Show-RestoreBackupDialog {
param([string]$BackupFilePath)
$scopeInfo = & $getStartMenuScopeInfo
$backupTargetText.Text = GetFriendlyRegistryBackupTarget -Target $scopeInfo.Target
$backupTargetText.Text = Get-FriendlyRegistryBackupTarget -Target $scopeInfo.Target
$overviewSummaryText.Text = "This will replace the current Start Menu pinned apps layout for $($scopeInfo.SummaryText) with the selected backup."
$backupFileText.Text = Split-Path -Path $BackupFilePath -Leaf
@@ -276,7 +276,7 @@ function Show-RestoreBackupDialog {
$backupFileText.Text = Split-Path $SelectedBackupFilePath -Leaf
$backupCreatedText.Text = $createdText
$backupTargetText.Text = GetFriendlyRegistryBackupTarget -Target ([string]$SelectedBackup.Target)
$backupTargetText.Text = Get-FriendlyRegistryBackupTarget -Target ([string]$SelectedBackup.Target)
$featuresItemsControl.ItemsSource = $revertibleFeaturesList
$overviewFeaturesSection.Visibility = if ($revertibleFeaturesList.Count -gt 0) { 'Visible' } else { 'Collapsed' }
$reappliedFeaturesItemsControl.ItemsSource = $reappliedFeaturesList
@@ -318,7 +318,7 @@ function Show-RestoreBackupDialog {
Write-Host "Backup file selected: $($openDialog.FileName)"
try {
$selectedBackup = Load-RegistryBackupFromFile -FilePath $openDialog.FileName
$selectedBackup = Import-RegistryBackup -FilePath $openDialog.FileName
if (-not (& $showRegistryOverview -SelectedBackup $selectedBackup -SelectedBackupFilePath $openDialog.FileName)) {
return
@@ -366,7 +366,7 @@ function Show-RestoreBackupDialog {
}
$window.Tag = @{
Result = 'RestoreStartMenu'
Result = 'Restore-StartMenu'
StartMenuScope = $scope
UseManualBackupFile = $useManualBackupFile
BackupFilePath = $state.SelectedStartMenuBackupFilePath
+7 -3
View File
@@ -1,3 +1,7 @@
<#
.SYNOPSIS
Shows the backup-restore dialog and performs the selected restore.
#>
function Show-RestoreBackupWindow {
param(
[System.Windows.Window]$Owner = $null
@@ -38,7 +42,7 @@ function Show-RestoreBackupWindow {
}
}
}
elseif ($dialogResult.Result -eq 'RestoreStartMenu') {
elseif ($dialogResult.Result -eq 'Restore-StartMenu') {
$scope = $dialogResult.StartMenuScope
$useManualBackupFile = ($dialogResult.UseManualBackupFile -eq $true)
$backupFilePath = $null
@@ -54,10 +58,10 @@ function Show-RestoreBackupWindow {
}
$result = if ($scope -eq 'AllUsers') {
RestoreStartMenuForAllUsers -BackupFilePath $backupFilePath
Restore-StartMenuForAllUsers -BackupFilePath $backupFilePath
}
else {
RestoreStartMenu -BackupFilePath $backupFilePath
Restore-StartMenu -BackupFilePath $backupFilePath
}
$resultEntries = @($result)
@@ -1,5 +1,8 @@
# Add parameter to script and write to file
function AddParameter {
<#
.SYNOPSIS
Adds or updates a value in the active parameter collection.
#>
function Add-Parameter {
param (
$parameterName,
$value = $true
@@ -39,25 +39,6 @@ function Convert-RegOperationToValueKind {
}
}
function Remove-RegistrySubKeyTreeIfExists {
param(
[Parameter(Mandatory)]
[Microsoft.Win32.RegistryKey]$RootKey,
[Parameter(Mandatory)]
[string]$SubKeyPath
)
try {
$RootKey.DeleteSubKeyTree($SubKeyPath, $false)
}
catch [System.UnauthorizedAccessException], [System.Security.SecurityException] {
throw
}
catch {
# Best-effort cleanup only; missing keys are fine.
}
}
function Get-RegistryKeyForOperation {
param(
[Parameter(Mandatory)]
@@ -1,7 +1,8 @@
# Shows confirmation dialogs for apps that require extra caution before removal.
# Returns $true if the user confirmed all warnings (or if no warnings were triggered),
# $false if the user declined any warning.
function ConfirmUnsafeAppRemoval {
<#
.SYNOPSIS
Confirms removal of applications that require an extra safety warning.
#>
function Confirm-UnsafeAppRemoval {
param (
[string[]]$SelectedApps,
$Owner = $null
@@ -1,5 +1,8 @@
# Generates a list of apps to remove based on the Apps parameter
function GenerateAppsList {
<#
.SYNOPSIS
Builds the validated application-removal list from the Apps parameter.
#>
function Generate-AppsList {
if (-not ($script:Params["Apps"] -and $script:Params["Apps"] -is [string])) {
return @()
}
@@ -8,12 +11,12 @@ function GenerateAppsList {
switch ($appMode) {
'default' {
$appsList = LoadAppsFromFile $script:AppsListFilePath
$appsList = Import-AppsFromFile $script:AppsListFilePath
return $appsList
}
default {
$appsList = $script:Params["Apps"].Split(',') | ForEach-Object { $_.Trim() }
$validatedAppsList = ValidateAppslist $appsList
$validatedAppsList = Get-ValidatedAppList $appsList
return $validatedAppsList
}
}
@@ -1,4 +1,8 @@
function GetFriendlyRegistryBackupTarget {
<#
.SYNOPSIS
Converts a registry-backup target identifier into a user-friendly label.
#>
function Get-FriendlyRegistryBackupTarget {
param(
[AllowNull()]
[AllowEmptyString()]
@@ -40,4 +44,4 @@ function GetFriendlyRegistryBackupTarget {
}
return $Target
}
}
@@ -0,0 +1,13 @@
<#
.SYNOPSIS
Returns a readable description of the current app-removal target.
#>
function Get-FriendlyTargetUserName {
$target = Get-TargetUserForAppRemoval
switch ($target) {
"AllUsers" { return "all users" }
"CurrentUser" { return "the current user" }
default { return "user $target" }
}
}
+31 -6
View File
@@ -74,7 +74,10 @@ function Get-RegFileOperations {
}
$parsedValue = Convert-RegValueData -valueData $matches.valueData.Trim()
if (-not $parsedValue) { continue }
if (-not $parsedValue) {
Write-Warning "Skipping unsupported or malformed registry value '$valueName' in '$currentKeyPath'."
continue
}
$operations += [PSCustomObject]@{
OperationType = $parsedValue.OperationType
@@ -88,6 +91,10 @@ function Get-RegFileOperations {
return $operations
}
<#
.SYNOPSIS
Converts a .reg value literal into an operation type, registry value type, and data.
#>
function Convert-RegValueData {
param(
[Parameter(Mandatory)]
@@ -121,8 +128,13 @@ function Convert-RegValueData {
}
if ($valueData -match '^hex(?:\((?<kind>[0-9a-fA-F]+)\))?:(?<bytes>[0-9a-fA-F,\s]+)$') {
$bytes = Convert-HexStringToByteArray -hexValue $matches.bytes
$parsedBytes = Convert-HexStringToByteArray -hexValue $matches.bytes
if ($null -eq $parsedBytes) {
return $null
}
$bytes = [byte[]]@($parsedBytes)
$valueType = if ($matches.kind) { "Hex$($matches.kind)" } else { 'Binary' }
$value = switch ($matches.kind) {
'2' { Convert-RegistryByteArrayToString -byteData $bytes }
'7' { Convert-RegistryByteArrayToMultiString -byteData $bytes }
@@ -150,16 +162,29 @@ function Convert-RegValueData {
return $null
}
<#
.SYNOPSIS
Converts a comma-separated hexadecimal byte string into a byte array.
#>
function Convert-HexStringToByteArray {
param(
[Parameter(Mandatory)]
[string]$hexValue
)
$parts = $hexValue.Split(',') | ForEach-Object { $_.Trim() } | Where-Object { $_ }
return [System.Linq.Enumerable]::Select($parts, [Func[object, byte]] {
param($h) [System.Convert]::ToByte($h, 16)
}) -as [byte[]]
$parts = @($hexValue.Split(',') | ForEach-Object { $_.Trim() })
if ($parts | Where-Object { [string]::IsNullOrWhiteSpace($_) }) {
return $null
}
$bytes = New-Object byte[] $parts.Count
for ($i = 0; $i -lt $parts.Count; $i++) {
if ($parts[$i] -notmatch '^[0-9a-fA-F]{1,2}$') {
return $null
}
$bytes[$i] = [System.Convert]::ToByte($parts[$i], 16)
}
return ,$bytes
}
function Convert-RegistryByteArrayToString {
@@ -1,6 +1,6 @@
# Target is determined from $script:Params["AppRemovalTarget"] or defaults to "AllUsers"
# Target values: "AllUsers" (removes for all users + from image), "CurrentUser", or a specific username
function GetTargetUserForAppRemoval {
function Get-TargetUserForAppRemoval {
if ($script:Params.ContainsKey("AppRemovalTarget")) {
return $script:Params["AppRemovalTarget"]
}
@@ -1,5 +1,5 @@
# Returns the directory path of the specified user, exits script if user path can't be found
function GetUserDirectory {
function Get-UserDirectory {
param (
$userName,
$fileName = "",
@@ -29,7 +29,7 @@ function GetUserDirectory {
}
}
$userContext = ResolveUserProfileContext -UserName $userName
$userContext = Resolve-UserProfileContext -UserName $userName
$resolvedUserDirectory = if ($userContext) { $userContext.ProfilePath } else { $null }
if ($resolvedUserDirectory) {
$userPath = if ([string]::IsNullOrWhiteSpace($fileName)) {
@@ -46,9 +46,9 @@ function GetUserDirectory {
}
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
Wait-ForKeyPress
}
Write-Error "Unable to find user directory path for user $userName"
AwaitKeyToExit
Wait-ForKeyPress
}
+11
View File
@@ -0,0 +1,11 @@
<#
.SYNOPSIS
Returns the explicitly targeted user name or the current process user name.
#>
function Get-UserName {
if ($script:Params.ContainsKey("User")) {
return $script:Params.Item("User")
}
return $env:USERNAME
}
@@ -1,9 +0,0 @@
function GetFriendlyTargetUserName {
$target = GetTargetUserForAppRemoval
switch ($target) {
"AllUsers" { return "all users" }
"CurrentUser" { return "the current user" }
default { return "user $target" }
}
}
-7
View File
@@ -1,7 +0,0 @@
function GetUserName {
if ($script:Params.ContainsKey("User")) {
return $script:Params.Item("User")
}
return $env:USERNAME
}
@@ -1,4 +1,8 @@
function ImportConfigToParams {
<#
.SYNOPSIS
Imports valid application, tweak, and deployment selections from a configuration JSON file into active parameters.
#>
function Import-ConfigToParams {
param (
[Parameter(Mandatory = $true)]
[string]$ConfigPath,
@@ -22,7 +26,7 @@ function ImportConfigToParams {
throw "Provided config file must be a .json file: $resolvedConfigPath"
}
$configJson = LoadJsonFile -filePath $resolvedConfigPath -expectedVersion $ExpectedVersion
$configJson = Import-JsonFile -filePath $resolvedConfigPath -expectedVersion $ExpectedVersion
if ($null -eq $configJson) {
throw "Failed to read config file: $resolvedConfigPath"
}
@@ -38,8 +42,8 @@ function ImportConfigToParams {
)
if ($appIds.Count -gt 0) {
AddParameter 'RemoveApps'
AddParameter 'Apps' ($appIds -join ',')
Add-Parameter 'RemoveApps'
Add-Parameter 'Apps' ($appIds -join ',')
$importedItems++
}
}
@@ -59,7 +63,7 @@ function ImportConfigToParams {
continue
}
AddParameter $setting.Name $true
Add-Parameter $setting.Name $true
$importedItems++
}
}
@@ -73,12 +77,12 @@ function ImportConfigToParams {
}
if ($deploymentLookup.ContainsKey('CreateRestorePoint') -and [bool]$deploymentLookup['CreateRestorePoint']) {
AddParameter 'CreateRestorePoint'
Add-Parameter 'CreateRestorePoint'
$importedItems++
}
if ($deploymentLookup.ContainsKey('RestartExplorer') -and -not [bool]$deploymentLookup['RestartExplorer']) {
AddParameter 'NoRestartExplorer'
Add-Parameter 'NoRestartExplorer'
$importedItems++
}
@@ -87,12 +91,12 @@ function ImportConfigToParams {
1 {
$otherUserName = if ($deploymentLookup.ContainsKey('OtherUsername')) { "$($deploymentLookup['OtherUsername'])".Trim() } else { '' }
if (-not [string]::IsNullOrWhiteSpace($otherUserName)) {
AddParameter 'User' $otherUserName
Add-Parameter 'User' $otherUserName
$importedItems++
}
}
2 {
AddParameter 'Sysprep'
Add-Parameter 'Sysprep'
$importedItems++
}
}
@@ -101,17 +105,17 @@ function ImportConfigToParams {
if ($deploymentLookup.ContainsKey('AppRemovalScopeIndex') -and $script:Params.ContainsKey('RemoveApps')) {
switch ([int]$deploymentLookup['AppRemovalScopeIndex']) {
0 {
AddParameter 'AppRemovalTarget' 'AllUsers'
Add-Parameter 'AppRemovalTarget' 'AllUsers'
$importedItems++
}
1 {
AddParameter 'AppRemovalTarget' 'CurrentUser'
Add-Parameter 'AppRemovalTarget' 'CurrentUser'
$importedItems++
}
2 {
$targetUser = if ($deploymentLookup.ContainsKey('OtherUsername')) { "$($deploymentLookup['OtherUsername'])".Trim() } else { '' }
if (-not [string]::IsNullOrWhiteSpace($targetUser)) {
AddParameter 'AppRemovalTarget' $targetUser
Add-Parameter 'AppRemovalTarget' $targetUser
$importedItems++
}
}
@@ -1,3 +1,7 @@
<#
.SYNOPSIS
Normalizes a rooted registry path and returns its hive and subkey components.
#>
function Split-RegistryPath {
param(
[Parameter(Mandatory)]
@@ -51,6 +55,10 @@ function Split-RegistryPath {
}
}
<#
.SYNOPSIS
Returns the .NET registry root key for a supported registry hive name.
#>
function Get-RegistryRootKey {
param(
[Parameter(Mandatory)]
@@ -67,6 +75,39 @@ function Get-RegistryRootKey {
}
}
<#
.SYNOPSIS
Deletes a registry subkey tree and ignores a key that has already disappeared.
#>
function Remove-RegistrySubKeyTreeIfExists {
param(
[Parameter(Mandatory)]
$RootKey,
[Parameter(Mandatory)]
[string]$SubKeyPath
)
try {
$RootKey.DeleteSubKeyTree($SubKeyPath, $false)
}
catch {
$failure = $_.Exception
while ($failure.InnerException) {
$failure = $failure.InnerException
}
if ($failure -is [System.ArgumentException]) {
# The key can disappear between snapshot inspection and deletion.
return
}
throw
}
}
<#
.SYNOPSIS
Returns a feature's registry-file path, using the Sysprep layout when targeting another profile.
#>
function Get-RegistryFilePathForFeature {
param(
[Parameter(Mandatory)]
@@ -12,7 +12,7 @@
.OUTPUTS
System.String
#>
function NormalizeUserLookupValue {
function Normalize-UserLookupValue {
param(
[string]$Value
)
@@ -44,12 +44,12 @@ if (-not $script:ResolvedUserSidCache) {
.OUTPUTS
System.String
#>
function GetUserLookupCacheKey {
function Get-UserLookupCacheKey {
param(
[string]$Value
)
$normalizedValue = NormalizeUserLookupValue -Value $Value
$normalizedValue = Normalize-UserLookupValue -Value $Value
if ([string]::IsNullOrWhiteSpace($normalizedValue)) {
return ''
}
@@ -72,13 +72,13 @@ function GetUserLookupCacheKey {
.OUTPUTS
System.String[]
#>
function GetNormalizedLookupCandidates {
function Get-NormalizedLookupCandidates {
param(
[string[]]$Candidates
)
$normalized = @($Candidates) |
ForEach-Object { NormalizeUserLookupValue -Value $_ } |
ForEach-Object { Normalize-UserLookupValue -Value $_ } |
Where-Object { -not [string]::IsNullOrWhiteSpace($_) } |
Select-Object -Unique
@@ -100,7 +100,7 @@ function GetNormalizedLookupCandidates {
.OUTPUTS
System.String
#>
function EscapeWqlString {
function Escape-WqlString {
param(
[string]$Value
)
@@ -126,22 +126,22 @@ function EscapeWqlString {
.OUTPUTS
System.String
#>
function GetLocalUserNameSegment {
function Get-LocalUserNameSegment {
param(
[string]$UserName
)
$normalizedName = NormalizeUserLookupValue -Value $UserName
$normalizedName = Normalize-UserLookupValue -Value $UserName
if ([string]::IsNullOrWhiteSpace($normalizedName)) {
return ''
}
if ($normalizedName.Contains('\')) {
return NormalizeUserLookupValue -Value (($normalizedName -split '\\')[-1])
return Normalize-UserLookupValue -Value (($normalizedName -split '\\')[-1])
}
if ($normalizedName.Contains('@')) {
return NormalizeUserLookupValue -Value (($normalizedName -split '@')[0])
return Normalize-UserLookupValue -Value (($normalizedName -split '@')[0])
}
return $normalizedName
@@ -165,12 +165,12 @@ function GetLocalUserNameSegment {
.OUTPUTS
System.String
#>
function ResolveNetBiosDomainName {
function Resolve-NetBiosDomainName {
param(
[string]$RawDomain
)
$trimmed = NormalizeUserLookupValue -Value $RawDomain
$trimmed = Normalize-UserLookupValue -Value $RawDomain
if ([string]::IsNullOrWhiteSpace($trimmed)) {
return ''
}
@@ -194,7 +194,7 @@ function ResolveNetBiosDomainName {
}
if ($ntDomainInstance -and -not [string]::IsNullOrWhiteSpace($ntDomainInstance.DomainName)) {
$fromNtDomain = NormalizeUserLookupValue -Value $ntDomainInstance.DomainName
$fromNtDomain = Normalize-UserLookupValue -Value $ntDomainInstance.DomainName
if (-not [string]::IsNullOrWhiteSpace($fromNtDomain)) {
return $fromNtDomain
}
@@ -205,7 +205,7 @@ function ResolveNetBiosDomainName {
}
if ($trimmed.Contains('.')) {
$leaf = NormalizeUserLookupValue -Value (($trimmed -split '\.')[0])
$leaf = Normalize-UserLookupValue -Value (($trimmed -split '\.')[0])
if (-not [string]::IsNullOrWhiteSpace($leaf)) {
return $leaf
}
@@ -221,7 +221,7 @@ function ResolveNetBiosDomainName {
.DESCRIPTION
Cached in script scope for the process lifetime. Returns $false on
error or workgroup. When joined, also caches the NetBIOS domain label
(ResolveNetBiosDomainName) for use as a profile-folder suffix.
(Resolve-NetBiosDomainName) for use as a profile-folder suffix.
.OUTPUTS
System.Boolean
@@ -239,7 +239,7 @@ function Test-MachineIsDomainJoined {
$computerSystem = Get-CimInstance -ClassName Win32_ComputerSystem -ErrorAction Stop
if ($null -ne $computerSystem -and $computerSystem.PartOfDomain) {
$script:MachineIsDomainJoined = $true
$script:MachineNetBiosDomain = ResolveNetBiosDomainName -RawDomain ([string]$computerSystem.Domain)
$script:MachineNetBiosDomain = Resolve-NetBiosDomainName -RawDomain ([string]$computerSystem.Domain)
}
}
catch {
@@ -262,7 +262,7 @@ function Test-MachineIsDomainJoined {
.OUTPUTS
System.String
#>
function GetProfileFolderDomainSuffix {
function Get-ProfileFolderDomainSuffix {
if (-not (Test-MachineIsDomainJoined)) {
return ''
}
@@ -301,12 +301,12 @@ function GetProfileFolderDomainSuffix {
.OUTPUTS
System.String[]
#>
function GetUserNameMatchCandidates {
function Get-UserNameMatchCandidates {
param(
[string]$Value
)
$normalized = NormalizeUserLookupValue -Value $Value
$normalized = Normalize-UserLookupValue -Value $Value
if ([string]::IsNullOrWhiteSpace($normalized)) {
return @()
}
@@ -314,13 +314,13 @@ function GetUserNameMatchCandidates {
$candidates = New-Object 'System.Collections.Generic.List[string]'
[void]$candidates.Add($normalized)
$localSegment = GetLocalUserNameSegment -UserName $normalized
$localSegment = Get-LocalUserNameSegment -UserName $normalized
if (-not [string]::IsNullOrWhiteSpace($localSegment) -and ($localSegment -ine $normalized)) {
[void]$candidates.Add($localSegment)
}
# Domain-suffixed forms only apply where Windows writes them.
$domainSuffix = GetProfileFolderDomainSuffix
$domainSuffix = Get-ProfileFolderDomainSuffix
if (-not [string]::IsNullOrWhiteSpace($domainSuffix)) {
# Prefer the local segment as the stem so DOMAIN\user still yields
# user.CONTOSO rather than the fully qualified string.
@@ -340,7 +340,7 @@ function GetUserNameMatchCandidates {
# (registry, backup metadata), so add that form too.
$suffixWithDot = ".$domainSuffix"
if ($normalized.Length -gt $suffixWithDot.Length -and $normalized.EndsWith($suffixWithDot, [System.StringComparison]::OrdinalIgnoreCase)) {
$bareStem = NormalizeUserLookupValue -Value ($normalized.Substring(0, $normalized.Length - $suffixWithDot.Length))
$bareStem = Normalize-UserLookupValue -Value ($normalized.Substring(0, $normalized.Length - $suffixWithDot.Length))
if (-not [string]::IsNullOrWhiteSpace($bareStem)) {
$alreadyPresent = $false
foreach ($existing in $candidates) {
@@ -361,7 +361,7 @@ function GetUserNameMatchCandidates {
Test whether a user name and a profile folder leaf share an account.
.DESCRIPTION
Compares candidate sets (via GetUserNameMatchCandidates) instead of
Compares candidate sets (via Get-UserNameMatchCandidates) instead of
raw strings, so different forms of the same account still match.
.PARAMETER UserName
@@ -383,8 +383,8 @@ function Test-UserNameMatchesProfileLeaf {
return $false
}
$leafCandidates = @(GetUserNameMatchCandidates -Value $ProfileLeaf)
$userCandidates = @(GetUserNameMatchCandidates -Value $UserName)
$leafCandidates = @(Get-UserNameMatchCandidates -Value $ProfileLeaf)
$userCandidates = @(Get-UserNameMatchCandidates -Value $UserName)
foreach ($leaf in $leafCandidates) {
foreach ($user in $userCandidates) {
@@ -430,13 +430,13 @@ function Test-UserNameMatch {
# Workgroup: strict equality (no suffix disambiguation available).
if (-not (Test-MachineIsDomainJoined)) {
$normalizedA = NormalizeUserLookupValue -Value $UserNameA
$normalizedB = NormalizeUserLookupValue -Value $UserNameB
$normalizedA = Normalize-UserLookupValue -Value $UserNameA
$normalizedB = Normalize-UserLookupValue -Value $UserNameB
return ($normalizedA -ieq $normalizedB)
}
$candidatesA = @(GetUserNameMatchCandidates -Value $UserNameA)
$candidatesB = @(GetUserNameMatchCandidates -Value $UserNameB)
$candidatesA = @(Get-UserNameMatchCandidates -Value $UserNameA)
$candidatesB = @(Get-UserNameMatchCandidates -Value $UserNameB)
foreach ($a in $candidatesA) {
foreach ($b in $candidatesB) {
@@ -463,7 +463,7 @@ function Test-UserNameMatch {
.PARAMETER Sid
Resolved SID to cache.
#>
function SetResolvedUserSidCache {
function Set-ResolvedUserSidCache {
param(
[string[]]$Candidates,
[string]$Sid
@@ -474,7 +474,7 @@ function SetResolvedUserSidCache {
}
foreach ($candidate in @($Candidates)) {
$cacheKey = GetUserLookupCacheKey -Value $candidate
$cacheKey = Get-UserLookupCacheKey -Value $candidate
if ($cacheKey) {
$script:ResolvedUserSidCache[$cacheKey] = $Sid
}
@@ -494,13 +494,13 @@ function SetResolvedUserSidCache {
.OUTPUTS
System.String
#>
function GetCachedResolvedUserSid {
function Get-CachedResolvedUserSid {
param(
[string[]]$Candidates
)
foreach ($candidate in @($Candidates)) {
$cacheKey = GetUserLookupCacheKey -Value $candidate
$cacheKey = Get-UserLookupCacheKey -Value $candidate
if ($cacheKey -and $script:ResolvedUserSidCache.ContainsKey($cacheKey)) {
return $script:ResolvedUserSidCache[$cacheKey]
}
@@ -523,7 +523,7 @@ function GetCachedResolvedUserSid {
.OUTPUTS
System.String
#>
function TryResolveSidByNtAccount {
function Try-ResolveSidByNtAccount {
param(
[string]$UserName
)
@@ -560,12 +560,12 @@ function TryResolveSidByNtAccount {
.OUTPUTS
System.String
#>
function TryResolveSidByLocalLookup {
function Try-ResolveSidByLocalLookup {
param(
[string[]]$Candidates
)
$lookupCandidates = GetNormalizedLookupCandidates -Candidates $Candidates
$lookupCandidates = Get-NormalizedLookupCandidates -Candidates $Candidates
if ($lookupCandidates.Count -eq 0) {
return $null
}
@@ -586,8 +586,8 @@ function TryResolveSidByLocalLookup {
foreach ($candidate in $lookupCandidates) {
try {
$escapedCandidate = EscapeWqlString -Value $candidate
$escapedComputerName = EscapeWqlString -Value $env:COMPUTERNAME
$escapedCandidate = Escape-WqlString -Value $candidate
$escapedComputerName = Escape-WqlString -Value $env:COMPUTERNAME
$filter = "LocalAccount=True AND (Name='$escapedCandidate' OR FullName='$escapedCandidate' OR Caption='$escapedComputerName\$escapedCandidate')"
$matchingAccount = Get-CimInstance -ClassName Win32_UserAccount -Filter $filter -ErrorAction Stop | Select-Object -First 1
@@ -617,12 +617,12 @@ function TryResolveSidByLocalLookup {
.OUTPUTS
System.String
#>
function TryResolveSidFromProfileList {
function Try-ResolveSidFromProfileList {
param(
[string[]]$Candidates
)
$lookupCandidates = GetNormalizedLookupCandidates -Candidates $Candidates
$lookupCandidates = Get-NormalizedLookupCandidates -Candidates $Candidates
if ($lookupCandidates.Count -eq 0) {
return $null
}
@@ -635,7 +635,7 @@ function TryResolveSidFromProfileList {
if ([string]::IsNullOrWhiteSpace($imagePath)) { continue }
$expandedPath = [System.Environment]::ExpandEnvironmentVariables($imagePath)
$leafName = NormalizeUserLookupValue -Value (Split-Path -Leaf $expandedPath)
$leafName = Normalize-UserLookupValue -Value (Split-Path -Leaf $expandedPath)
foreach ($candidate in $lookupCandidates) {
if (Test-MachineIsDomainJoined) {
@@ -680,7 +680,7 @@ function TryResolveSidFromProfileList {
.OUTPUTS
System.Management.Automation.PSCustomObject
#>
function NewResolvedUserContext {
function New-ResolvedUserContext {
param(
[string]$UserName,
[string]$UserSid,
@@ -708,12 +708,12 @@ function NewResolvedUserContext {
.OUTPUTS
System.String
#>
function GetQualifiedProcessIdentityName {
function Get-QualifiedProcessIdentityName {
param(
[string]$Candidate
)
$normalizedCandidate = NormalizeUserLookupValue -Value $Candidate
$normalizedCandidate = Normalize-UserLookupValue -Value $Candidate
if ([string]::IsNullOrWhiteSpace($normalizedCandidate)) {
return $null
}
@@ -735,7 +735,7 @@ function GetQualifiedProcessIdentityName {
return $null
}
$currentLocalSegment = GetLocalUserNameSegment -UserName $currentName
$currentLocalSegment = Get-LocalUserNameSegment -UserName $currentName
if (-not [string]::IsNullOrWhiteSpace($currentLocalSegment) -and $currentLocalSegment -ieq $normalizedCandidate) {
return $currentName
}
@@ -762,19 +762,19 @@ function GetQualifiedProcessIdentityName {
.OUTPUTS
System.String
#>
function ResolveUserSid {
function Resolve-UserSid {
param(
[Parameter(Mandatory)]
[string]$UserName
)
$candidateUserName = NormalizeUserLookupValue -Value $UserName
$candidateUserName = Normalize-UserLookupValue -Value $UserName
if ([string]::IsNullOrWhiteSpace($candidateUserName)) {
return $null
}
$hasQualifiedIdentity = $candidateUserName.Contains('\') -or $candidateUserName.Contains('@')
$localNameSegment = GetLocalUserNameSegment -UserName $candidateUserName
$localNameSegment = Get-LocalUserNameSegment -UserName $candidateUserName
$leafNameCandidates = @()
if ($hasQualifiedIdentity -and -not [string]::IsNullOrWhiteSpace($localNameSegment) -and $localNameSegment -ine $candidateUserName) {
$leafNameCandidates = @($localNameSegment)
@@ -796,7 +796,7 @@ function ResolveUserSid {
@($candidateUserName)
}
$cachedSid = GetCachedResolvedUserSid -Candidates $lookupCandidates
$cachedSid = Get-CachedResolvedUserSid -Candidates $lookupCandidates
if ($cachedSid) {
return $cachedSid
}
@@ -811,12 +811,12 @@ function ResolveUserSid {
}
elseif (Test-MachineIsDomainJoined) {
# Prefer process identity (authoritative), then USERDOMAIN\input.
$processQualifiedName = GetQualifiedProcessIdentityName -Candidate $candidateUserName
$processQualifiedName = Get-QualifiedProcessIdentityName -Candidate $candidateUserName
if (-not [string]::IsNullOrWhiteSpace($processQualifiedName)) {
[void]$qualifiedNamesToTry.Add($processQualifiedName)
}
$domainSuffix = GetProfileFolderDomainSuffix
$domainSuffix = Get-ProfileFolderDomainSuffix
if (-not [string]::IsNullOrWhiteSpace($domainSuffix)) {
$domainQualifiedName = "$domainSuffix\$candidateUserName"
if (-not ($qualifiedNamesToTry -contains $domainQualifiedName)) {
@@ -831,10 +831,10 @@ function ResolveUserSid {
# Step 2: resolve qualified form(s) via NTAccount.Translate.
foreach ($qualifiedName in $qualifiedNamesToTry) {
$resolvedSid = TryResolveSidByNtAccount -UserName $qualifiedName
$resolvedSid = Try-ResolveSidByNtAccount -UserName $qualifiedName
if ($resolvedSid) {
$allCacheKeys = @($candidateUserName) + $qualifiedNamesToTry | Select-Object -Unique
SetResolvedUserSidCache -Candidates $allCacheKeys -Sid $resolvedSid
Set-ResolvedUserSidCache -Candidates $allCacheKeys -Sid $resolvedSid
return $resolvedSid
}
}
@@ -842,20 +842,20 @@ function ResolveUserSid {
# Step 3: local SAM fallback (workgroup only; skipped on domain to avoid
# nameshare shadowing).
if (-not (Test-MachineIsDomainJoined)) {
$resolvedSid = TryResolveSidByLocalLookup -Candidates $lookupCandidates
$resolvedSid = Try-ResolveSidByLocalLookup -Candidates $lookupCandidates
if ($resolvedSid) {
$allCacheKeys = @($candidateUserName) + $qualifiedNamesToTry | Select-Object -Unique
SetResolvedUserSidCache -Candidates $allCacheKeys -Sid $resolvedSid
Set-ResolvedUserSidCache -Candidates $allCacheKeys -Sid $resolvedSid
return $resolvedSid
}
}
# Step 4: ProfileList leaf heuristic (last resort; disambiguates by
# on-disk folder name, suffix-aware on domain boxes).
$resolvedSid = TryResolveSidFromProfileList -Candidates $profileHeuristicCandidates
$resolvedSid = Try-ResolveSidFromProfileList -Candidates $profileHeuristicCandidates
if ($resolvedSid) {
$allCacheKeys = @($candidateUserName) + $qualifiedNamesToTry | Select-Object -Unique
SetResolvedUserSidCache -Candidates $allCacheKeys -Sid $resolvedSid
Set-ResolvedUserSidCache -Candidates $allCacheKeys -Sid $resolvedSid
return $resolvedSid
}
@@ -878,13 +878,13 @@ function ResolveUserSid {
.OUTPUTS
System.Management.Automation.PSCustomObject
#>
function ResolveUserProfileContext {
function Resolve-UserProfileContext {
param(
[Parameter(Mandatory)]
[string]$UserName
)
$candidateUserName = NormalizeUserLookupValue -Value $UserName
$candidateUserName = Normalize-UserLookupValue -Value $UserName
if ([string]::IsNullOrWhiteSpace($candidateUserName)) {
return $null
}
@@ -902,14 +902,14 @@ function ResolveUserProfileContext {
$defaultProfilePath = Join-Path $rootPath 'Default'
if (Test-Path -LiteralPath $defaultProfilePath -PathType Container) {
return (NewResolvedUserContext -UserName $candidateUserName -UserSid $null -ProfilePath $defaultProfilePath)
return (New-ResolvedUserContext -UserName $candidateUserName -UserSid $null -ProfilePath $defaultProfilePath)
}
}
return $null
}
$userSid = ResolveUserSid -UserName $candidateUserName
$userSid = Resolve-UserSid -UserName $candidateUserName
if ($userSid) {
$sidRegistryPath = "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\$userSid"
@@ -919,7 +919,7 @@ function ResolveUserProfileContext {
if (-not [string]::IsNullOrWhiteSpace($registryImagePath)) {
$expandedPath = [System.Environment]::ExpandEnvironmentVariables($registryImagePath)
if (Test-Path -LiteralPath $expandedPath -PathType Container) {
return (NewResolvedUserContext -UserName $candidateUserName -UserSid $userSid -ProfilePath $expandedPath)
return (New-ResolvedUserContext -UserName $candidateUserName -UserSid $userSid -ProfilePath $expandedPath)
}
}
}
@@ -932,7 +932,7 @@ function ResolveUserProfileContext {
$matchingProfiles = @(Get-CimInstance -ClassName Win32_UserProfile -Filter "SID='$userSid'" -ErrorAction Stop)
$resolvedProfile = $matchingProfiles | Where-Object { -not [string]::IsNullOrWhiteSpace($_.LocalPath) } | Select-Object -First 1
if ($resolvedProfile -and (Test-Path -LiteralPath $resolvedProfile.LocalPath -PathType Container)) {
return (NewResolvedUserContext -UserName $candidateUserName -UserSid $userSid -ProfilePath $resolvedProfile.LocalPath)
return (New-ResolvedUserContext -UserName $candidateUserName -UserSid $userSid -ProfilePath $resolvedProfile.LocalPath)
}
}
catch {
@@ -948,7 +948,7 @@ function ResolveUserProfileContext {
# Exact leaf match first (common case; avoids an unnecessary scan).
$candidateUserPath = Join-Path $rootPath $candidateUserName
if (Test-Path -LiteralPath $candidateUserPath -PathType Container) {
return (NewResolvedUserContext -UserName $candidateUserName -UserSid $userSid -ProfilePath $candidateUserPath)
return (New-ResolvedUserContext -UserName $candidateUserName -UserSid $userSid -ProfilePath $candidateUserPath)
}
# Only domain-joined boxes write suffixed folders; scanning workgroup
@@ -957,7 +957,7 @@ function ResolveUserProfileContext {
try {
foreach ($child in @(Get-ChildItem -LiteralPath $rootPath -Directory -ErrorAction SilentlyContinue)) {
if (Test-UserNameMatchesProfileLeaf -UserName $candidateUserName -ProfileLeaf $child.Name) {
return (NewResolvedUserContext -UserName $candidateUserName -UserSid $userSid -ProfilePath $child.FullName)
return (New-ResolvedUserContext -UserName $candidateUserName -UserSid $userSid -ProfilePath $child.FullName)
}
}
}
@@ -1,5 +1,5 @@
# Check if this machine supports S0 Modern Standby power state. Returns true if S0 Modern Standby is supported, false otherwise.
function CheckModernStandbySupport {
function Test-ModernStandbySupport {
$count = 0
try {
+1 -1
View File
@@ -23,7 +23,7 @@ function Test-TargetUserName {
}
}
if (-not (CheckIfUserExists -userName $normalizedUserName)) {
if (-not (Test-UserProfileExists -userName $normalizedUserName)) {
return [PSCustomObject]@{
IsValid = $false
UserName = $normalizedUserName
@@ -1,4 +1,4 @@
function CheckIfUserExists {
function Test-UserProfileExists {
param (
[string]$userName
)
@@ -10,7 +10,7 @@ function CheckIfUserExists {
$lookupName = $userName.Trim()
# Validate special characters against the local username segment (user in DOMAIN\user or user@domain).
$localUserName = GetLocalUserNameSegment -UserName $lookupName
$localUserName = Get-LocalUserNameSegment -UserName $lookupName
if ($localUserName.IndexOfAny([System.IO.Path]::GetInvalidFileNameChars()) -ge 0) {
return $false
@@ -22,7 +22,7 @@ function CheckIfUserExists {
}
try {
$userContext = ResolveUserProfileContext -UserName $lookupName
$userContext = Resolve-UserProfileContext -UserName $lookupName
if (-not $userContext -or [string]::IsNullOrWhiteSpace($userContext.ProfilePath)) {
return $false
}
@@ -31,12 +31,12 @@ function Resolve-TargetUserHiveContext {
[string]$TargetUserName
)
$normalizedTargetUserName = NormalizeUserLookupValue -Value $TargetUserName
$normalizedTargetUserName = Normalize-UserLookupValue -Value $TargetUserName
if ([string]::IsNullOrWhiteSpace($normalizedTargetUserName)) {
throw 'Target user name for registry hive resolution is empty.'
}
$userContext = ResolveUserProfileContext -UserName $normalizedTargetUserName
$userContext = Resolve-UserProfileContext -UserName $normalizedTargetUserName
if (-not $userContext -or [string]::IsNullOrWhiteSpace([string]$userContext.ProfilePath)) {
throw "Unable to resolve profile path for target user '$normalizedTargetUserName'."
}
+49
View File
@@ -0,0 +1,49 @@
<#
.SYNOPSIS
Runs the Win11Debloat Pester test suite locally.
.PARAMETER Bootstrap
Installs a compatible Pester 5 release for the current user when Pester is
not already available.
#>
[CmdletBinding()]
param(
[switch]$Bootstrap
)
$ErrorActionPreference = 'Stop'
$repositoryRoot = Split-Path -Parent $PSScriptRoot
$testPath = Join-Path $repositoryRoot 'Tests'
if (-not (Get-Module -ListAvailable -Name Pester | Where-Object { $_.Version.Major -eq 5 })) {
if (-not $Bootstrap) {
Write-Error 'Pester 5 is required. Install it with: .\Scripts\Run-Tests.ps1 -Bootstrap'
exit 1
}
try {
[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
if (-not (Get-PackageProvider -Name NuGet -ListAvailable -ErrorAction SilentlyContinue)) {
Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Scope CurrentUser -Force -Confirm:$false | Out-Null
}
Install-Module -Name Pester -RequiredVersion 5.9.0 -Scope CurrentUser -Force -AllowClobber -Confirm:$false
}
catch {
Write-Error "Unable to install Pester 5: $($_.Exception.Message)"
exit 1
}
if (-not (Get-Module -ListAvailable -Name Pester | Where-Object { $_.Version.Major -eq 5 })) {
Write-Error 'Pester 5 was not installed. Update PowerShellGet or install Pester 5 manually, then run the tests again.'
exit 1
}
}
Import-Module Pester -MinimumVersion 5.0.0 -MaximumVersion 5.999.999 -Force
$result = Invoke-Pester -Path $testPath -Output Detailed -PassThru
if ($result.Result -ne 'Passed' -or $result.FailedContainersCount -gt 0 -or $result.TotalCount -eq 0) {
exit 1
}
@@ -1,6 +1,6 @@
# Processes all pending WPF window messages (input, render, etc.) to keep the UI responsive
# during long-running operations on the UI thread. Equivalent to Application.DoEvents().
function DoEvents {
# during long-running operations on the UI thread. Equivalent to Application.Invoke-DoEvents().
function Invoke-DoEvents {
if (-not $script:GuiWindow) { return }
$frame = [System.Windows.Threading.DispatcherFrame]::new()
$null = [System.Windows.Threading.Dispatcher]::CurrentDispatcher.BeginInvoke(
+1 -1
View File
@@ -31,7 +31,7 @@ function Invoke-NonBlocking {
$ps.Stop()
throw "Operation timed out after $TimeoutSeconds seconds"
}
DoEvents
Invoke-DoEvents
Start-Sleep -Milliseconds 16
}
}