Compare commits

18 Commits

Author SHA1 Message Date
Jeffrey
12f3ce401b Add icon font fallback 2026-05-31 15:10:18 +02:00
Jeffrey
924772c3a0 Update completion message 2026-05-31 14:14:41 +02:00
Jeffrey
68248b4a04 Add support for undo features in registry backup capture and validation processes 2026-05-30 21:28:52 +02:00
Jeffrey
1ed967b9d3 Add DisableWhenApplied property to features and update UI control states accordingly 2026-05-30 21:23:45 +02:00
Jeffrey
4332eaa833 Remove redundant undo action logic for comboboxes 2026-05-30 16:13:48 +02:00
Jeffrey
9deeb295e7 Add support for QWord registry value type in Get-ExpectedRegistryValueKind function 2026-05-28 23:43:17 +02:00
Jeffrey
f6ed6ac487 Add default case to Invoke-UndoFeatureAction for undefined feature IDs 2026-05-28 23:26:40 +02:00
Jeffrey
b920536be2 Add ValueKind validation and fix string unescaping in Convert-RegValueData 2026-05-28 23:25:27 +02:00
Jeffrey
7273f29fea Clean up 2026-05-28 22:55:38 +02:00
Jeffrey
7381c29da2 Clean up access control checks for re-enabling Store search suggestions 2026-05-28 22:30:18 +02:00
Jeffrey
3bed9cafbc Lock user mode selection when Automatically check currently applied tweaks is checked 2026-05-27 23:33:35 +02:00
Jeffrey
6dbaac0513 Properly show re-applied features in registry backup overview, properly show applied tweaks checkbox state after registry backup restoration 2026-05-27 22:05:06 +02:00
Jeffrey
6e63b34dbb Update success message for registry restore 2026-05-27 21:49:42 +02:00
Jeffrey
3f763b01ab Clean up 2026-05-27 21:48:07 +02:00
Jeffrey
4109588e0f Add option to show & undo applied tweaks 2026-05-27 21:36:07 +02:00
Jeffrey
37872b2030 Update CONTRIBUTING.md 2026-05-26 15:57:34 +02:00
Jeffrey
abfc5db2c3 Improve log output in Get.ps1 and clean up file exclusions 2026-05-25 14:35:39 +02:00
Jeffrey
1d828d6a78 Fix typo in Disable_Game_Bar_Integration Sysprep registry file 2026-05-24 14:53:12 +02:00
25 changed files with 1157 additions and 174 deletions

View File

@@ -109,7 +109,7 @@ Avoid these common mistakes when contributing:
Placing files in the wrong directory will cause the script to fail when trying to apply or undo changes. Placing files in the wrong directory will cause the script to fail when trying to apply or undo changes.
6. **Not Testing Undo Functionality**: Always test that your undo registry file properly reverts all changes. A feature that can't be undone will frustrate users. 6. **Not Testing Undo Functionality**: Always test that your undo registry file properly reverts all changes.
7. **Not Testing User/Sysprep Functionality**: Always test that your feature works when applied to another user or to the Windows default user with Sysprep. Sysprep changes can be tested by creating new users after running the script. 7. **Not Testing User/Sysprep Functionality**: Always test that your feature works when applied to another user or to the Windows default user with Sysprep. Sysprep changes can be tested by creating new users after running the script.

View File

@@ -863,7 +863,8 @@
"ApplyText": null, "ApplyText": null,
"RegistryUndoKey": null, "RegistryUndoKey": null,
"MinVersion": null, "MinVersion": null,
"MaxVersion": null "MaxVersion": null,
"DisableWhenApplied": true
}, },
{ {
"FeatureId": "HideChat", "FeatureId": "HideChat",

View File

@@ -732,6 +732,8 @@
<ColumnDefinition Width="Auto"/> <ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/> <ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="10"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" Orientation="Horizontal"> <StackPanel Grid.Column="0" Orientation="Horizontal">
@@ -798,7 +800,9 @@
</Popup> </Popup>
</StackPanel> </StackPanel>
<Border x:Name="TweakSearchBorder" Grid.Column="2"> <CheckBox x:Name="ShowCurrentlyAppliedTweaksCheckBox" Grid.Column="2" Content="Automatically check currently applied tweaks" IsChecked="True" Foreground="{DynamicResource FgColor}" VerticalAlignment="Center" AutomationProperties.Name="Check applied tweaks" ToolTip="Check all tweaks currently active on this system for the current user. Unchecking clears all selections."/>
<Border x:Name="TweakSearchBorder" Grid.Column="4">
<Border.Style> <Border.Style>
<Style TargetType="Border" BasedOn="{StaticResource SearchBoxBorderStyle}"> <Style TargetType="Border" BasedOn="{StaticResource SearchBoxBorderStyle}">
<Style.Triggers> <Style.Triggers>

View File

@@ -222,6 +222,8 @@
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Grid Grid.Row="0" Margin="0,0,0,16"> <Grid Grid.Row="0" Margin="0,0,0,16">
@@ -282,9 +284,38 @@
Visibility="Collapsed" Visibility="Collapsed"
Text="This will restore the Start Menu pinned apps layout for the current user."/> Text="This will restore the Start Menu pinned apps layout for the current user."/>
<Border x:Name="NonRevertibleSeparator" Grid.Row="3" Height="1" Background="{DynamicResource BorderColor}" Margin="0,12,0,12" Visibility="Collapsed"/> <Border x:Name="ReappliedSeparator" Grid.Row="3" Height="1" Background="{DynamicResource BorderColor}" Margin="0,12,0,12" Visibility="Collapsed"/>
<Grid x:Name="NonRevertiblePanel" Grid.Row="4" Visibility="Collapsed"> <Grid x:Name="ReappliedPanel" Grid.Row="4" Visibility="Collapsed">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0"
Text="The following changes will be re-applied:"
FontSize="13"
Foreground="{DynamicResource FgColor}"
FontWeight="SemiBold"
Margin="0,0,0,8"/>
<ItemsControl x:Name="ReappliedFeaturesItemsControl" Grid.Row="1">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical" IsItemsHost="True"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding DisplayText}" FontSize="12" Foreground="{DynamicResource FgColor}" TextWrapping="Wrap" Margin="0,0,0,6"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
<Border x:Name="NonRevertibleSeparator" Grid.Row="5" Height="1" Background="{DynamicResource BorderColor}" Margin="0,12,0,12" Visibility="Collapsed"/>
<Grid x:Name="NonRevertiblePanel" Grid.Row="6" Visibility="Collapsed">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="*"/> <RowDefinition Height="*"/>

View File

@@ -14,8 +14,7 @@ function Get-FeatureId {
function Get-RegistryBackedFeatures { function Get-RegistryBackedFeatures {
param( param(
[Parameter(Mandatory)] [object[]]$Features = @()
[object[]]$Features
) )
return @($Features | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_.RegistryKey) }) return @($Features | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_.RegistryKey) })

View File

@@ -1,47 +1,44 @@
function Get-RegistryBackupCapturePlans { function Get-RegistryBackupCapturePlans {
param( param(
[Parameter(Mandatory)] [object[]]$SelectedRegistryFeatures = @(),
[object[]]$SelectedRegistryFeatures, [object[]]$UndoRegistryFeatures = @(),
[switch]$UseSysprepRegFiles [switch]$UseSysprepRegFiles
) )
$planMap = @{} $planMap = @{}
foreach ($feature in $SelectedRegistryFeatures) { foreach ($feature in $SelectedRegistryFeatures) {
$regFilePath = Get-RegistryFilePathForFeature -Feature $feature -UseSysprepRegFiles:$UseSysprepRegFiles $regFilePath = Get-RegistryFilePathForFeature -RegistryKey $feature.RegistryKey -UseSysprepRegFiles:$UseSysprepRegFiles
if (-not (Test-Path $regFilePath)) { if (-not (Test-Path $regFilePath)) {
throw "Unable to find registry file for backup: $($feature.RegistryKey) ($regFilePath)" throw "Unable to find registry file for backup: $($feature.RegistryKey) ($regFilePath)"
} }
foreach ($operation in @(Get-RegFileOperations -regFilePath $regFilePath)) { foreach ($operation in @(Get-RegFileOperations -regFilePath $regFilePath)) {
if (-not $operation.KeyPath) { continue } if (-not $operation.KeyPath) { continue }
Add-RegistryPlanOperation -PlanMap $planMap -Operation $operation
}
}
$mapKey = $operation.KeyPath.ToLowerInvariant() foreach ($feature in $UndoRegistryFeatures) {
if (-not $planMap.ContainsKey($mapKey)) { $regFilePath = Resolve-RegistryBackupUndoFilePath -Feature $feature
$planMap[$mapKey] = [PSCustomObject]@{ if ([string]::IsNullOrWhiteSpace($regFilePath)) {
Path = $operation.KeyPath continue
IncludeSubKeys = $false }
CaptureAllValues = $false
ValueNames = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase) if (-not (Test-Path $regFilePath)) {
} $undoKeyDescription = if (-not [string]::IsNullOrWhiteSpace([string]$feature.RegistryUndoKey)) {
[string]$feature.RegistryUndoKey
}
else {
[string]$feature.RegistryKey
} }
$plan = $planMap[$mapKey] throw "Unable to find registry undo file for backup: $undoKeyDescription ($regFilePath)"
switch ($operation.OperationType) { }
'DeleteKey' {
$plan.IncludeSubKeys = $true foreach ($operation in @(Get-RegFileOperations -regFilePath $regFilePath)) {
$plan.CaptureAllValues = $true if (-not $operation.KeyPath) { continue }
} Add-RegistryPlanOperation -PlanMap $planMap -Operation $operation
'SetValue' {
if (-not $plan.CaptureAllValues) {
$null = $plan.ValueNames.Add([string]$operation.ValueName)
}
}
'DeleteValue' {
if (-not $plan.CaptureAllValues) {
$null = $plan.ValueNames.Add([string]$operation.ValueName)
}
}
}
} }
} }
@@ -57,10 +54,68 @@ function Get-RegistryBackupCapturePlans {
) )
} }
function Get-RegistrySnapshotsForBackup { function Add-RegistryPlanOperation {
param(
[hashtable]$PlanMap,
[PSCustomObject]$Operation
)
$mapKey = $Operation.KeyPath.ToLowerInvariant()
if (-not $PlanMap.ContainsKey($mapKey)) {
$PlanMap[$mapKey] = [PSCustomObject]@{
Path = $Operation.KeyPath
IncludeSubKeys = $false
CaptureAllValues = $false
ValueNames = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
}
}
$plan = $PlanMap[$mapKey]
switch ($Operation.OperationType) {
'DeleteKey' {
$plan.IncludeSubKeys = $true
$plan.CaptureAllValues = $true
}
'SetValue' {
if (-not $plan.CaptureAllValues) {
$null = $plan.ValueNames.Add([string]$Operation.ValueName)
}
}
'DeleteValue' {
if (-not $plan.CaptureAllValues) {
$null = $plan.ValueNames.Add([string]$Operation.ValueName)
}
}
}
}
function Resolve-RegistryBackupUndoFilePath {
param( param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
[object[]]$CapturePlans $Feature
)
$undoRegistryKey = [string]$Feature.RegistryUndoKey
if (-not [string]::IsNullOrWhiteSpace($undoRegistryKey)) {
$resolvedUndoPath = Resolve-UndoRegFilePath -FileName $undoRegistryKey
return Join-Path $script:RegfilesPath $resolvedUndoPath
}
$resolvedRegistryKey = [string]$Feature.RegistryKey
if ([string]::IsNullOrWhiteSpace($resolvedRegistryKey)) {
return $null
}
if ([System.IO.Path]::IsPathRooted($resolvedRegistryKey)) {
return $resolvedRegistryKey
}
return Join-Path $script:RegfilesPath $resolvedRegistryKey
}
function Get-RegistrySnapshotsForBackup {
param(
[object[]]$CapturePlans = @()
) )
if ($CapturePlans.Count -eq 0) { if ($CapturePlans.Count -eq 0) {

View File

@@ -1,11 +1,14 @@
function New-RegistrySettingsBackup { function New-RegistrySettingsBackup {
param( param(
[string[]]$ActionableKeys [string[]]$ActionableKeys,
[object[]]$ExtraFeatures = @()
) )
$ActionableKeys = @($ActionableKeys) $ActionableKeys = @($ActionableKeys)
$selectedFeatures = Get-SelectedFeatures -ActionableKeys $ActionableKeys $selectedFeatures = @(Get-SelectedFeatures -ActionableKeys $ActionableKeys)
if (@($selectedFeatures | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_.RegistryKey) }).Count -eq 0) { $undoFeatures = @($ExtraFeatures | Where-Object { $_ -ne $null })
$allFeatures = @($selectedFeatures) + @($undoFeatures)
if (@($allFeatures | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_.RegistryKey) }).Count -eq 0) {
return $null return $null
} }
@@ -18,7 +21,7 @@ function New-RegistrySettingsBackup {
$backupFileName = 'Win11Debloat-RegistryBackup-{0}.json' -f $timestamp.ToString('yyyyMMdd_HHmmss') $backupFileName = 'Win11Debloat-RegistryBackup-{0}.json' -f $timestamp.ToString('yyyyMMdd_HHmmss')
$backupFilePath = Join-Path $backupDirectory $backupFileName $backupFilePath = Join-Path $backupDirectory $backupFileName
$backupConfig = Get-RegistryBackupPayload -SelectedFeatures $selectedFeatures -CreatedAt $timestamp $backupConfig = Get-RegistryBackupPayload -SelectedFeatures $selectedFeatures -UndoFeatures $undoFeatures -CreatedAt $timestamp
if (-not (SaveToFile -Config $backupConfig -FilePath $backupFilePath -MaxDepth 25)) { if (-not (SaveToFile -Config $backupConfig -FilePath $backupFilePath -MaxDepth 25)) {
throw "Failed to save registry backup to '$backupFilePath'" throw "Failed to save registry backup to '$backupFilePath'"
} }
@@ -55,8 +58,8 @@ function Get-SelectedFeatures {
function Get-RegistryBackupPayload { function Get-RegistryBackupPayload {
param( param(
[Parameter(Mandatory)] [object[]]$SelectedFeatures = @(),
[object[]]$SelectedFeatures, [object[]]$UndoFeatures = @(),
[Parameter(Mandatory)] [Parameter(Mandatory)]
[datetime]$CreatedAt [datetime]$CreatedAt
) )
@@ -71,11 +74,24 @@ function Get-RegistryBackupPayload {
} }
} }
$selectedRegistryFeatures = Get-RegistryBackedFeatures -Features $SelectedFeatures $selectedUndoFeatureIds = New-Object System.Collections.Generic.List[string]
$capturePlans = Get-RegistryBackupCapturePlans -SelectedRegistryFeatures $SelectedRegistryFeatures $seenUndoFeatureIds = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
foreach ($feature in $UndoFeatures) {
$featureId = Get-FeatureId -Feature $feature
if ($seenUndoFeatureIds.Add($featureId)) {
$selectedUndoFeatureIds.Add($featureId)
}
}
$selectedRegistryFeatures = @(Get-RegistryBackedFeatures -Features $SelectedFeatures)
$undoRegistryFeatures = @($UndoFeatures | Where-Object {
-not [string]::IsNullOrWhiteSpace([string]$_.RegistryUndoKey) -or -not [string]::IsNullOrWhiteSpace([string]$_.RegistryKey)
})
$capturePlans = @(Get-RegistryBackupCapturePlans -SelectedRegistryFeatures $selectedRegistryFeatures -UndoRegistryFeatures $undoRegistryFeatures)
$registryKeys = @(Get-RegistrySnapshotsForBackup -CapturePlans $capturePlans) $registryKeys = @(Get-RegistrySnapshotsForBackup -CapturePlans $capturePlans)
return @{ $backupPayload = @{
Version = '1.0' Version = '1.0'
BackupType = 'RegistryState' BackupType = 'RegistryState'
CreatedAt = $CreatedAt.ToString('o') CreatedAt = $CreatedAt.ToString('o')
@@ -85,4 +101,10 @@ function Get-RegistryBackupPayload {
SelectedFeatures = @($selectedFeatureIds) SelectedFeatures = @($selectedFeatureIds)
RegistryKeys = @($registryKeys) RegistryKeys = @($registryKeys)
} }
if ($selectedUndoFeatureIds.Count -gt 0) {
$backupPayload['SelectedUndoFeatures'] = @($selectedUndoFeatureIds)
}
return $backupPayload
} }

View File

@@ -50,3 +50,129 @@ function DisableStoreSearchSuggestions {
Write-Host "Disabled Microsoft Store search suggestions for user $userName" Write-Host "Disabled Microsoft Store search suggestions for user $userName"
} }
function EnableStoreSearchSuggestionsForAllUsers {
# Get path to Store app database for all users
$userPathString = GetUserDirectory -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 ($storeDbPath.FullName + "\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db")
}
# Also re-enable for the default user profile
$defaultStoreDbPath = GetUserDirectory -userName "Default" -fileName "AppData\Local\Packages\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db" -exitIfPathNotFound $false
EnableStoreSearchSuggestions $defaultStoreDbPath
}
function EnableStoreSearchSuggestions {
param (
$StoreAppsDatabase = "$env:LocalAppData\Packages\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db"
)
# Change path to correct user if a user was specified
if ($script:Params.ContainsKey("User")) {
$StoreAppsDatabase = GetUserDirectory -userName "$(GetUserName)" -fileName "AppData\Local\Packages\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db" -exitIfPathNotFound $false
}
$userName = [regex]::Match($StoreAppsDatabase, '(?:Users\\)([^\\]+)(?:\\AppData)').Groups[1].Value
if (-not $userName) { $userName = '<unknown>' }
if (-not (Test-Path -Path $StoreAppsDatabase)) {
Write-Host "Store app database not found for user $userName, nothing to undo"
return
}
# Ensure we can modify/delete the file even if restrictive ACLs were set.
$global:LASTEXITCODE = 0
takeown /F "$StoreAppsDatabase" /A | Out-Null
icacls "$StoreAppsDatabase" /grant *S-1-5-32-544:F /C | Out-Null
$everyoneSid = [System.Security.Principal.SecurityIdentifier]::new('S-1-1-0') # 'EVERYONE' group
try {
$acl = Get-Acl -Path $StoreAppsDatabase
$denyRules = @(
$acl.Access | Where-Object {
$_.AccessControlType -eq [System.Security.AccessControl.AccessControlType]::Deny -and
(($_.FileSystemRights -band [System.Security.AccessControl.FileSystemRights]::FullControl) -ne 0) -and
(try { $_.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]) -eq $everyoneSid } catch { $false })
}
)
foreach ($denyRule in $denyRules) {
$null = $acl.RemoveAccessRuleSpecific($denyRule)
}
Set-Acl -Path $StoreAppsDatabase -AclObject $acl | Out-Null
}
catch {
Write-Warning "Failed to normalize ACL for store database '$StoreAppsDatabase': $($_.Exception.Message)"
}
try {
Remove-Item -Path $StoreAppsDatabase -Force -ErrorAction Stop
Write-Host "Re-enabled Microsoft Store search suggestions for user $userName"
}
catch {
throw "Failed to remove '$StoreAppsDatabase' while undoing Microsoft Store search suggestions for user $userName. $($_.Exception.Message)"
}
}
function Test-StoreSearchSuggestionsDisabled {
param(
[Parameter(Mandatory)]
[string]$StoreAppsDatabase
)
if (-not (Test-Path -Path $StoreAppsDatabase)) {
return $false
}
try {
$acl = Get-Acl -Path $StoreAppsDatabase
}
catch {
return $false
}
$everyoneSid = [System.Security.Principal.SecurityIdentifier]::new('S-1-1-0')
foreach ($accessRule in @($acl.Access)) {
if ($accessRule.AccessControlType -eq [System.Security.AccessControl.AccessControlType]::Deny -and
(($accessRule.FileSystemRights -band [System.Security.AccessControl.FileSystemRights]::FullControl) -ne 0) -and
(try { $accessRule.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]) -eq $everyoneSid } catch { $false })) {
return $true
}
}
return $false
}
function Test-StoreSearchSuggestionsDisabledForAllUsers {
$paths = @()
$userPathString = GetUserDirectory -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 = GetUserDirectory -userName "Default" -fileName "AppData\Local\Packages\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db" -exitIfPathNotFound $false
if ($defaultStoreDbPath) {
$paths += $defaultStoreDbPath
}
if ($paths.Count -eq 0) {
return $false
}
foreach ($path in $paths) {
if (-not (Test-StoreSearchSuggestionsDisabled -StoreAppsDatabase $path)) {
return $false
}
}
return $true
}

View File

@@ -14,3 +14,36 @@ function EnableWindowsFeature {
Write-Host ($dismResult | Out-String).Trim() Write-Host ($dismResult | Out-String).Trim()
} }
} }
# Disables a Windows optional feature and pipes its output to the console
function DisableWindowsFeature {
param (
[string]$FeatureName
)
$result = Invoke-NonBlocking -ScriptBlock {
param($name)
Disable-WindowsOptionalFeature -Online -FeatureName $name -NoRestart
} -ArgumentList $FeatureName
$dismResult = @($result) | Where-Object { $_ -is [Microsoft.Dism.Commands.ImageObject] }
if ($dismResult) {
Write-Host ($dismResult | Out-String).Trim()
}
}
function Test-WindowsOptionalFeatureEnabled {
param (
[Parameter(Mandatory)]
[string]$FeatureName
)
try {
$feature = Get-WindowsOptionalFeature -Online -FeatureName $FeatureName -ErrorAction Stop
}
catch {
return $false
}
return ($feature.State -eq 'Enabled')
}

View File

@@ -1,3 +1,55 @@
# Resolves the path of an undo reg file relative to $script:RegfilesPath.
# Checks the Undo/ subfolder first, then falls back to the root Regfiles/ folder.
function Resolve-UndoRegFilePath {
param ([string]$FileName)
$undoSubPath = Join-Path 'Undo' $FileName
if (Test-Path (Join-Path $script:RegfilesPath $undoSubPath)) {
return $undoSubPath
}
return $FileName
}
function Invoke-UndoFeatureAction {
param(
[Parameter(Mandatory)]
[string]$FeatureId
)
switch ($FeatureId) {
'DisableStoreSearchSuggestions' {
if ($script:Params.ContainsKey('Sysprep')) {
Write-Host "> Re-enabling Microsoft Store search suggestions in the start menu for all users..."
EnableStoreSearchSuggestionsForAllUsers
Write-Host ""
return
}
Write-Host "> Re-enabling Microsoft Store search suggestions for user $(GetUserName)..."
EnableStoreSearchSuggestions
Write-Host ""
return
}
'EnableWindowsSandbox' {
Write-Host "> Disabling Windows Sandbox..."
DisableWindowsFeature 'Containers-DisposableClientVM'
Write-Host ""
return
}
'EnableWindowsSubsystemForLinux' {
Write-Host "> Disabling Windows Subsystem for Linux..."
DisableWindowsFeature 'Microsoft-Windows-Subsystem-Linux'
DisableWindowsFeature 'VirtualMachinePlatform'
Write-Host ""
return
}
default {
Write-Host "> No undo action defined for $FeatureId, skipping..." -ForegroundColor Yellow
Write-Host ""
return
}
}
}
# Executes a single parameter/feature based on its key # Executes a single parameter/feature based on its key
# Parameters: # Parameters:
# $paramKey - The parameter name to execute # $paramKey - The parameter name to execute
@@ -162,8 +214,15 @@ function ExecuteAllChanges {
break break
} }
} }
# Undo operations that write registry values also require a backup
if (-not $hasRegistryBackedFeature) {
foreach ($featureId in $script:UndoParams.Keys) {
$f = if ($script:Features.ContainsKey($featureId)) { $script:Features[$featureId] } else { $null }
if ($f -and $f.RegistryUndoKey) { $hasRegistryBackedFeature = $true; break }
}
}
$totalSteps = $actionableKeys.Count $totalSteps = $actionableKeys.Count + $script:UndoParams.Count
if ($hasRegistryBackedFeature) { $totalSteps++ } if ($hasRegistryBackedFeature) { $totalSteps++ }
if ($script:Params.ContainsKey("CreateRestorePoint")) { $totalSteps++ } if ($script:Params.ContainsKey("CreateRestorePoint")) { $totalSteps++ }
$currentStep = 0 $currentStep = 0
@@ -176,7 +235,13 @@ function ExecuteAllChanges {
Write-Host "> Creating registry backup..." Write-Host "> Creating registry backup..."
try { try {
New-RegistrySettingsBackup -ActionableKeys $actionableKeys | Out-Null $undoSyntheticFeatures = @($script:UndoParams.Keys | ForEach-Object {
$f = if ($script:Features.ContainsKey($_)) { $script:Features[$_] } else { $null }
if ($f -and $f.RegistryUndoKey) {
[PSCustomObject]@{ FeatureId = $_; RegistryKey = (Resolve-UndoRegFilePath $f.RegistryUndoKey) }
}
} | Where-Object { $_ })
New-RegistrySettingsBackup -ActionableKeys $actionableKeys -ExtraFeatures $undoSyntheticFeatures | Out-Null
} }
catch { catch {
throw "Registry backup failed before applying changes. $($_.Exception.Message)" throw "Registry backup failed before applying changes. $($_.Exception.Message)"
@@ -222,6 +287,26 @@ function ExecuteAllChanges {
ExecuteParameter -paramKey $paramKey ExecuteParameter -paramKey $paramKey
} }
# Execute all undo operations
foreach ($featureId in $script:UndoParams.Keys) {
if ($script:CancelRequested) { return }
$undoLabel = if ($script:FeatureLabelLookup) { $script:FeatureLabelLookup[$featureId] } else { $null }
if (-not $undoLabel) { $undoLabel = $featureId }
$currentStep++
if ($script:ApplyProgressCallback) {
& $script:ApplyProgressCallback $currentStep $totalSteps "Undoing: $undoLabel"
}
$f = if ($script:Features.ContainsKey($featureId)) { $script:Features[$featureId] } else { $null }
if ($f -and $f.RegistryUndoKey) {
ImportRegistryFile "> Undoing: $undoLabel" (Resolve-UndoRegFilePath $f.RegistryUndoKey)
} else {
Invoke-UndoFeatureAction -FeatureId $featureId
}
}
if ($script:RegistryImportFailures -gt 0) { if ($script:RegistryImportFailures -gt 0) {
Write-Host "" Write-Host ""
Write-Host "$($script:RegistryImportFailures) registry import change(s) failed. See output above for details." -ForegroundColor Yellow Write-Host "$($script:RegistryImportFailures) registry import change(s) failed. See output above for details." -ForegroundColor Yellow

View File

@@ -0,0 +1,175 @@
# Tests whether the registry operations in a feature's .reg file currently match the live registry.
# Returns $true if ALL operations in the apply reg file match current system state.
# Returns $false if the feature has no RegistryKey, the file is missing, or any operation mismatches.
function Get-ExpectedRegistryValueKind {
param(
[Parameter(Mandatory)]
$Operation
)
switch ([string]$Operation.ValueType) {
'DWord' { return [Microsoft.Win32.RegistryValueKind]::DWord }
'QWord' { return [Microsoft.Win32.RegistryValueKind]::QWord }
'String' { return [Microsoft.Win32.RegistryValueKind]::String }
'Binary' { return [Microsoft.Win32.RegistryValueKind]::Binary }
'Hex2' { return [Microsoft.Win32.RegistryValueKind]::ExpandString }
'Hex7' { return [Microsoft.Win32.RegistryValueKind]::MultiString }
default { return $null }
}
}
function Test-FeatureApplied {
param (
[Parameter(Mandatory)]
[string]$FeatureId
)
if (-not $script:Features.ContainsKey($FeatureId)) { return $false }
$feature = $script:Features[$FeatureId]
switch ($FeatureId) {
'DisableWidgets' {
# Widgets packages cannot be reinstalled automatically, so we treat their
# absence as the applied state (checked) and presence as not-yet-applied.
$widgetAppIds = @(
'Microsoft.StartExperiencesApp',
'MicrosoftWindows.Client.WebExperience',
'Microsoft.WidgetsPlatformRuntime'
)
foreach ($appId in $widgetAppIds) {
if (Get-AppxPackage -Name $appId -AllUsers -ErrorAction SilentlyContinue) {
return $false
}
}
return $true
}
'DisableStoreSearchSuggestions' {
if ($script:Params.ContainsKey('Sysprep')) {
return (Test-StoreSearchSuggestionsDisabledForAllUsers)
}
$storeDbPath = "$env:LocalAppData\Packages\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db"
if ($script:Params.ContainsKey('User')) {
$storeDbPath = GetUserDirectory -userName "$(GetUserName)" -fileName "AppData\Local\Packages\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db" -exitIfPathNotFound $false
}
return (Test-StoreSearchSuggestionsDisabled -StoreAppsDatabase $storeDbPath)
}
'EnableWindowsSandbox' {
return (Test-WindowsOptionalFeatureEnabled -FeatureName 'Containers-DisposableClientVM')
}
'EnableWindowsSubsystemForLinux' {
$wslEnabled = Test-WindowsOptionalFeatureEnabled -FeatureName 'Microsoft-Windows-Subsystem-Linux'
$vmpEnabled = Test-WindowsOptionalFeatureEnabled -FeatureName 'VirtualMachinePlatform'
return ($wslEnabled -and $vmpEnabled)
}
}
if (-not $feature.RegistryKey) { return $false }
$regFilePath = Join-Path $script:RegfilesPath $feature.RegistryKey
if (-not (Test-Path $regFilePath)) { return $false }
try {
$operations = @(Get-RegFileOperations -regFilePath $regFilePath)
}
catch { return $false }
if ($operations.Count -eq 0) { return $false }
foreach ($op in $operations) {
$parts = Split-RegistryPath -path $op.KeyPath
if (-not $parts) { return $false }
$rootKey = Get-RegistryRootKey -hiveName $parts.Hive
if (-not $rootKey) { return $false }
$key = $null
try {
$key = $rootKey.OpenSubKey($parts.SubKey, $false)
switch ($op.OperationType) {
'DeleteKey' {
if ($null -ne $key) { return $false }
}
'DeleteValue' {
if ($null -ne $key) {
$names = @($key.GetValueNames())
if ($names -icontains $op.ValueName) { return $false }
}
# key missing = value also gone = operation matches
}
'SetValue' {
if ($null -eq $key) { return $false }
$names = @($key.GetValueNames())
if (-not ($names -icontains $op.ValueName)) { return $false }
$actualKind = $key.GetValueKind($op.ValueName)
$expectedKind = Get-ExpectedRegistryValueKind -Operation $op
if ($null -eq $expectedKind -or $actualKind -ne $expectedKind) { return $false }
$actualRaw = $key.GetValue($op.ValueName, $null, [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)
$actual = switch ($actualKind) {
([Microsoft.Win32.RegistryValueKind]::DWord) {
[BitConverter]::ToUInt32([BitConverter]::GetBytes([int32]$actualRaw), 0)
}
([Microsoft.Win32.RegistryValueKind]::QWord) {
[BitConverter]::ToUInt64([BitConverter]::GetBytes([int64]$actualRaw), 0)
}
([Microsoft.Win32.RegistryValueKind]::Binary) {
@($actualRaw | ForEach-Object { [int]$_ })
}
([Microsoft.Win32.RegistryValueKind]::MultiString) {
@($actualRaw)
}
default {
if ($null -ne $actualRaw) { [string]$actualRaw } else { $null }
}
}
$expected = $op.ValueData
$match = if (($actual -is [array]) -and ($expected -is [array])) {
(Compare-Object $actual $expected).Count -eq 0
} else {
$actual -eq $expected
}
if (-not $match) { return $false }
}
}
}
catch { return $false }
finally {
if ($null -ne $key) { $key.Close() }
}
}
return $true
}
# Returns the 1-based index of the UiGroup option whose features all match current system state,
# or 0 if no option fully matches (meaning the current state is unknown / "No Change").
function Get-CurrentGroupActiveIndex {
param (
[Parameter(Mandatory)]
[object]$Group
)
$i = 1
foreach ($val in $Group.Values) {
$allApplied = $true
foreach ($fid in $val.FeatureIds) {
if (-not (Test-FeatureApplied -FeatureId $fid)) {
$allApplied = $false
break
}
}
if ($allApplied) { return $i }
$i++
}
return 0
}

View File

@@ -8,13 +8,7 @@ function ImportRegistryFile {
Write-Host $message Write-Host $message
$usesOfflineHive = $script:Params.ContainsKey("Sysprep") -or $script:Params.ContainsKey("User") $usesOfflineHive = $script:Params.ContainsKey("Sysprep") -or $script:Params.ContainsKey("User")
$regFileDirectory = if ($usesOfflineHive) { $regFilePath = Get-RegistryFilePathForFeature -RegistryKey $path
Join-Path $script:RegfilesPath "Sysprep"
}
else {
$script:RegfilesPath
}
$regFilePath = Join-Path $regFileDirectory $path
if (-not (Test-Path $regFilePath)) { if (-not (Test-Path $regFilePath)) {
$errorMessage = "Unable to find registry file: $path ($regFilePath)" $errorMessage = "Unable to find registry file: $path ($regFilePath)"

View File

@@ -33,12 +33,49 @@ function Get-NormalizedSelectedFeatureIdsFromBackup {
$errors.Add('SelectedFeatures must contain non-empty string feature IDs.') $errors.Add('SelectedFeatures must contain non-empty string feature IDs.')
} }
if ($selectedFeatures.Count -eq 0) { return [PSCustomObject]@{
$errors.Add('SelectedFeatures must contain at least one feature ID.') SelectedFeatures = $selectedFeatures.ToArray()
Errors = $errors.ToArray()
}
}
function Get-NormalizedSelectedUndoFeatureIdsFromBackup {
param(
[Parameter(Mandatory)]
$Backup
)
$selectedUndoFeatures = New-Object System.Collections.Generic.List[string]
$selectedUndoFeatureIds = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
$errors = New-Object System.Collections.Generic.List[string]
# SelectedUndoFeatures is optional - only process if present
if (-not $Backup.PSObject.Properties['SelectedUndoFeatures']) {
return [PSCustomObject]@{
SelectedUndoFeatures = $selectedUndoFeatures.ToArray()
Errors = $errors.ToArray()
}
}
$hasInvalidSelectedUndoFeatureId = $false
foreach ($featureId in @($Backup.SelectedUndoFeatures)) {
if ($featureId -isnot [string] -or [string]::IsNullOrWhiteSpace([string]$featureId)) {
$hasInvalidSelectedUndoFeatureId = $true
continue
}
$normalizedFeatureId = [string]$featureId
if ($selectedUndoFeatureIds.Add($normalizedFeatureId)) {
$selectedUndoFeatures.Add($normalizedFeatureId)
}
}
if ($hasInvalidSelectedUndoFeatureId) {
$errors.Add('SelectedUndoFeatures must contain non-empty string feature IDs.')
} }
return [PSCustomObject]@{ return [PSCustomObject]@{
SelectedFeatures = $selectedFeatures.ToArray() SelectedUndoFeatures = $selectedUndoFeatures.ToArray()
Errors = $errors.ToArray() Errors = $errors.ToArray()
} }
} }
@@ -96,6 +133,9 @@ function Test-RegistryBackupMatchesSelectedFeatures {
[AllowEmptyCollection()] [AllowEmptyCollection()]
[string[]]$SelectedFeatureIds, [string[]]$SelectedFeatureIds,
[Parameter(Mandatory)] [Parameter(Mandatory)]
[AllowEmptyCollection()]
[string[]]$SelectedUndoFeatureIds,
[Parameter(Mandatory)]
[string]$Target, [string]$Target,
[Parameter(Mandatory)] [Parameter(Mandatory)]
[AllowEmptyCollection()] [AllowEmptyCollection()]
@@ -109,18 +149,19 @@ function Test-RegistryBackupMatchesSelectedFeatures {
return $errors.ToArray() return $errors.ToArray()
} }
$selectedRegistryFeatures = @(Get-SelectedRegistryFeaturesForBackupValidation -SelectedFeatureIds @($SelectedFeatureIds) -Errors $errors) $selectedRegistryFeatures = @(Get-SelectedRegistryFeaturesForBackupValidation -SelectedFeatureIds @($SelectedFeatureIds) -IsUndoFeature:$false -Errors $errors)
$undoRegistryFeatures = @(Get-SelectedRegistryFeaturesForBackupValidation -SelectedFeatureIds @($SelectedUndoFeatureIds) -IsUndoFeature:$true -Errors $errors)
$useSysprepRegFiles = ($Target -eq 'DefaultUserProfile') -or ($Target -like 'User:*') $useSysprepRegFiles = ($Target -eq 'DefaultUserProfile') -or ($Target -like 'User:*')
$capturePlans = @() $capturePlans = @()
if ($errors.Count -eq 0 -and $selectedRegistryFeatures.Count -gt 0) { if ($errors.Count -eq 0 -and ($selectedRegistryFeatures.Count -gt 0 -or $undoRegistryFeatures.Count -gt 0)) {
$capturePlans = @(Get-RegistryBackupCapturePlans -SelectedRegistryFeatures @($selectedRegistryFeatures) -UseSysprepRegFiles:$useSysprepRegFiles) $capturePlans = @(Get-RegistryBackupCapturePlans -SelectedRegistryFeatures @($selectedRegistryFeatures) -UndoRegistryFeatures @($undoRegistryFeatures) -UseSysprepRegFiles:$useSysprepRegFiles)
} }
$planMap = New-RegistryBackupAllowListPlanMap -CapturePlans @($capturePlans) $planMap = New-RegistryBackupAllowListPlanMap -CapturePlans @($capturePlans)
if ($planMap.Count -eq 0 -and @($RegistryKeys).Count -gt 0) { if ($planMap.Count -eq 0 -and @($RegistryKeys).Count -gt 0) {
$errors.Add('Backup contains registry snapshots but no allowed registry paths were derived from SelectedFeatures.') $errors.Add('Backup contains registry snapshots but no allowed registry paths were derived from the selected features.')
} }
foreach ($rootSnapshot in @($RegistryKeys)) { foreach ($rootSnapshot in @($RegistryKeys)) {
@@ -136,6 +177,8 @@ function Get-SelectedRegistryFeaturesForBackupValidation {
[AllowEmptyCollection()] [AllowEmptyCollection()]
[string[]]$SelectedFeatureIds, [string[]]$SelectedFeatureIds,
[Parameter(Mandatory)] [Parameter(Mandatory)]
[bool]$IsUndoFeature,
[Parameter(Mandatory)]
[AllowEmptyCollection()] [AllowEmptyCollection()]
$Errors $Errors
) )
@@ -152,7 +195,26 @@ function Get-SelectedRegistryFeaturesForBackupValidation {
} }
$feature = $script:Features[$featureId] $feature = $script:Features[$featureId]
if ($feature -and -not [string]::IsNullOrWhiteSpace([string]$feature.RegistryKey)) { if (-not $feature) {
continue
}
# For undo features, check RegistryUndoKey if present (real features)
# Otherwise check RegistryKey (for synthetic features from backup capture)
$registryKeyToUse = if ($IsUndoFeature) {
$key = [string]$feature.RegistryUndoKey
if (-not [string]::IsNullOrWhiteSpace($key)) {
$key
}
else {
[string]$feature.RegistryKey
}
}
else {
[string]$feature.RegistryKey
}
if (-not [string]::IsNullOrWhiteSpace($registryKeyToUse)) {
$selectedRegistryFeatures.Add($feature) $selectedRegistryFeatures.Add($feature)
} }
} }

View File

@@ -87,7 +87,17 @@ function Normalize-RegistryBackup {
$errors.Add([string]$selectedFeatureParseError) $errors.Add([string]$selectedFeatureParseError)
} }
$allowListValidationErrors = @(Test-RegistryBackupMatchesSelectedFeatures -SelectedFeatureIds @($selectedFeatures) -Target $normalizedTarget -RegistryKeys @($normalizedKeys)) $selectedUndoFeatureParseResult = Get-NormalizedSelectedUndoFeatureIdsFromBackup -Backup $Backup
$selectedUndoFeatures = @($selectedUndoFeatureParseResult.SelectedUndoFeatures)
foreach ($selectedUndoFeatureParseError in @($selectedUndoFeatureParseResult.Errors)) {
$errors.Add([string]$selectedUndoFeatureParseError)
}
$allSelectedFeatures = @($selectedFeatures) + @($selectedUndoFeatures)
if ($allSelectedFeatures.Count -eq 0) {
$errors.Add('Backup must contain at least one feature ID in SelectedFeatures or SelectedUndoFeatures.')
}
$allowListValidationErrors = @(Test-RegistryBackupMatchesSelectedFeatures -SelectedFeatureIds @($selectedFeatures) -SelectedUndoFeatureIds @($selectedUndoFeatures) -Target $normalizedTarget -RegistryKeys @($normalizedKeys))
foreach ($allowListValidationError in $allowListValidationErrors) { foreach ($allowListValidationError in $allowListValidationErrors) {
$errors.Add([string]$allowListValidationError) $errors.Add([string]$allowListValidationError)
} }
@@ -110,6 +120,7 @@ function Normalize-RegistryBackup {
ComputerName = [string]$Backup.ComputerName ComputerName = [string]$Backup.ComputerName
Target = $normalizedTarget Target = $normalizedTarget
SelectedFeatures = @($selectedFeatures) SelectedFeatures = @($selectedFeatures)
SelectedUndoFeatures = @($selectedUndoFeatures)
RegistryKeys = @($normalizedKeys) RegistryKeys = @($normalizedKeys)
} }
} }

View File

@@ -79,16 +79,70 @@ function Test-RestoreDialogFeatureVisibleInOverview {
return -not [string]::IsNullOrWhiteSpace([string]$featureDefinition.Category) return -not [string]::IsNullOrWhiteSpace([string]$featureDefinition.Category)
} }
function Get-SelectedForwardFeatureIdsFromBackup {
param($SelectedBackup)
$selectedFeatureIds = New-Object System.Collections.Generic.List[string]
$seenSelectedFeatureIds = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
foreach ($featureId in @($SelectedBackup.SelectedFeatures)) {
if ([string]::IsNullOrWhiteSpace([string]$featureId)) {
continue
}
$normalizedId = [string]$featureId
if ($seenSelectedFeatureIds.Add($normalizedId)) {
$selectedFeatureIds.Add($normalizedId)
}
}
return @($selectedFeatureIds.ToArray())
}
function Get-SelectedUndoFeatureIdsFromBackup {
param($SelectedBackup)
$selectedUndoFeatureIds = New-Object System.Collections.Generic.List[string]
$seenUndoFeatureIds = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
foreach ($featureId in @($SelectedBackup.SelectedUndoFeatures)) {
if ([string]::IsNullOrWhiteSpace([string]$featureId)) {
continue
}
$normalizedId = [string]$featureId
if ($seenUndoFeatureIds.Add($normalizedId)) {
$selectedUndoFeatureIds.Add($normalizedId)
}
}
return @($selectedUndoFeatureIds.ToArray())
}
function Get-CombinedSelectedFeatureIdsFromBackup {
param($SelectedBackup)
$featureIds = New-Object System.Collections.Generic.List[string]
$seenIds = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
foreach ($featureId in @(Get-SelectedForwardFeatureIdsFromBackup -SelectedBackup $SelectedBackup) + @(Get-SelectedUndoFeatureIdsFromBackup -SelectedBackup $SelectedBackup)) {
if ([string]::IsNullOrWhiteSpace([string]$featureId)) {
continue
}
$normalizedId = [string]$featureId
if ($seenIds.Add($normalizedId)) {
$featureIds.Add($normalizedId)
}
}
return @($featureIds.ToArray())
}
function Get-SelectedFeatureIdsFromBackup { function Get-SelectedFeatureIdsFromBackup {
param($SelectedBackup) param($SelectedBackup)
return @( return @(Get-CombinedSelectedFeatureIdsFromBackup -SelectedBackup $SelectedBackup)
foreach ($featureId in @($SelectedBackup.SelectedFeatures)) {
if (-not [string]::IsNullOrWhiteSpace([string]$featureId)) {
[string]$featureId
}
}
)
} }
function Get-RestoreBackupFeatureLists { function Get-RestoreBackupFeatureLists {

View File

@@ -1,4 +1,71 @@
# Sets resource colors for a WPF window based on dark mode preference # Sets resource colors for a WPF window based on dark mode preference
function GetIconFontFamilyName {
if ($script:IconFontFamilyName) {
return $script:IconFontFamilyName
}
$preferredFont = 'Segoe Fluent Icons'
$fallbackFont = 'Segoe MDL2 Assets'
try {
$systemFonts = [System.Windows.Media.Fonts]::SystemFontFamilies | ForEach-Object { $_.Source }
if ($systemFonts -contains $preferredFont) {
$script:IconFontFamilyName = $preferredFont
}
elseif ($systemFonts -contains $fallbackFont) {
$script:IconFontFamilyName = $fallbackFont
}
else {
# Last resort fallback if the expected symbol fonts are unavailable.
$script:IconFontFamilyName = 'Segoe UI Symbol'
}
}
catch {
$script:IconFontFamilyName = $fallbackFont
}
return $script:IconFontFamilyName
}
function SetIconFontFallback {
param($window)
if (-not $window) {
return
}
$targetFontName = GetIconFontFamilyName
if ($targetFontName -eq 'Segoe Fluent Icons') {
return
}
$targetFontFamily = [System.Windows.Media.FontFamily]::new($targetFontName)
$queue = [System.Collections.Queue]::new()
$queue.Enqueue($window)
while ($queue.Count -gt 0) {
$node = $queue.Dequeue()
if ($node -is [System.Windows.Controls.TextBlock]) {
if ($node.FontFamily -and $node.FontFamily.Source -eq 'Segoe Fluent Icons') {
$node.FontFamily = $targetFontFamily
}
}
elseif ($node -is [System.Windows.Controls.Control]) {
if ($node.FontFamily -and $node.FontFamily.Source -eq 'Segoe Fluent Icons') {
$node.FontFamily = $targetFontFamily
}
}
foreach ($child in [System.Windows.LogicalTreeHelper]::GetChildren($node)) {
if ($child -is [System.Windows.DependencyObject]) {
$queue.Enqueue($child)
}
}
}
}
function SetWindowThemeResources { function SetWindowThemeResources {
param ( param (
$window, $window,
@@ -92,4 +159,6 @@ function SetWindowThemeResources {
$sharedReader.Close() $sharedReader.Close()
} }
} }
SetIconFontFallback -window $window
} }

View File

@@ -186,7 +186,7 @@ function Show-ApplyModal {
$applyRebootPanel.Visibility = 'Visible' $applyRebootPanel.Visibility = 'Visible'
} }
else { else {
$script:ApplyCompletionMessageEl.Text = "Your clean system is ready. Thanks for using Win11Debloat!" $script:ApplyCompletionMessageEl.Text = "Your system is ready. Thanks for using Win11Debloat!"
} }
} }
} }

View File

@@ -278,7 +278,15 @@ function Show-MainWindow {
if ($restoreBackupBtn) { if ($restoreBackupBtn) {
$restoreBackupBtn.Add_Click({ $restoreBackupBtn.Add_Click({
try { try {
Show-RestoreBackupWindow -Owner $window $restoreResult = Show-RestoreBackupWindow -Owner $window
if ($restoreResult -and $restoreResult.RestoredRegistry -eq $true) {
RefreshCurrentTweakSystemState -ApplyToUi:$false
if ($ShowCurrentlyAppliedTweaksCheckBox -and $ShowCurrentlyAppliedTweaksCheckBox.IsChecked -eq $true) {
ResetTweaksToSystemState -loadSystemState $true
UpdateTweakPresetStates
}
}
} }
catch { catch {
Write-Warning "Restore backup action failed: $($_.Exception.Message)" Write-Warning "Restore backup action failed: $($_.Exception.Message)"
@@ -383,6 +391,7 @@ function Show-MainWindow {
$control = $window.FindName($controlName) $control = $window.FindName($controlName)
if ($control -is [System.Windows.Controls.CheckBox]) { if ($control -is [System.Windows.Controls.CheckBox]) {
$control.IsChecked = $false $control.IsChecked = $false
$control.IsEnabled = $true
} }
elseif ($control -is [System.Windows.Controls.ComboBox]) { elseif ($control -is [System.Windows.Controls.ComboBox]) {
$control.SelectedIndex = 0 $control.SelectedIndex = 0
@@ -882,13 +891,19 @@ function Show-MainWindow {
$items = @('No Change', $opt) $items = @('No Change', $opt)
$comboName = ("Feature_{0}_Combo" -f $feature.FeatureId) -replace '[^a-zA-Z0-9_]','' $comboName = ("Feature_{0}_Combo" -f $feature.FeatureId) -replace '[^a-zA-Z0-9_]',''
$combo = CreateLabeledCombo -parent $panel -labelText $feature.Label -comboName $comboName -items $items $combo = CreateLabeledCombo -parent $panel -labelText $feature.Label -comboName $comboName -items $items
# attach tooltip from Features.json if present # attach tooltip from Features.json if present, and include the disabled-state reason
if ($feature.ToolTip) { if ($feature.ToolTip -or $feature.DisableWhenApplied -eq $true) {
$tooltipText = $feature.ToolTip
if ($feature.DisableWhenApplied -eq $true) {
$tooltipText = "This tweak is already applied and cannot be undone automatically. Visit the Win11Debloat wiki for instructions on how to manually revert this change."
}
$tipBlock = New-Object System.Windows.Controls.TextBlock $tipBlock = New-Object System.Windows.Controls.TextBlock
$tipBlock.Text = $feature.ToolTip $tipBlock.Text = $tooltipText
$tipBlock.TextWrapping = 'Wrap' $tipBlock.TextWrapping = 'Wrap'
$tipBlock.MaxWidth = 420 $tipBlock.MaxWidth = 420
$combo.ToolTip = $tipBlock $combo.ToolTip = $tipBlock
[System.Windows.Controls.ToolTipService]::SetShowOnDisabled($combo, $true)
$lblBorderObj = $null $lblBorderObj = $null
try { $lblBorderObj = $window.FindName("$comboName`_LabelBorder") } catch {} try { $lblBorderObj = $window.FindName("$comboName`_LabelBorder") } catch {}
if ($lblBorderObj) { $lblBorderObj.ToolTip = $tipBlock } if ($lblBorderObj) { $lblBorderObj.ToolTip = $tipBlock }
@@ -905,6 +920,65 @@ function Show-MainWindow {
} }
} }
function RefreshCurrentTweakSystemState {
param([bool]$ApplyToUi)
if (-not $script:UiControlMappings) { return }
if (-not $script:Features) { return }
$featuresJson = LoadJsonFile -filePath $script:FeaturesFilePath -expectedVersion "1.0"
if (-not $featuresJson) { return }
$groupMap = @{}
if ($featuresJson.UiGroups) {
foreach ($g in $featuresJson.UiGroups) {
$groupMap[$g.GroupId] = $g
}
}
foreach ($controlName in $script:UiControlMappings.Keys) {
$control = $window.FindName($controlName)
if (-not $control) { continue }
$mapping = $script:UiControlMappings[$controlName]
if ($control -is [System.Windows.Controls.CheckBox] -and $mapping.Type -eq 'feature') {
$applied = $false
try { $applied = [bool](Test-FeatureApplied -FeatureId $mapping.FeatureId) } catch {}
$featureObj = $script:Features[$mapping.FeatureId]
$disableWhenApplied = $featureObj -and $featureObj.DisableWhenApplied -eq $true
Add-Member -InputObject $control -MemberType NoteProperty -Name 'SystemState' -Value $applied -Force
Add-Member -InputObject $control -MemberType NoteProperty -Name 'DisableWhenApplied' -Value $disableWhenApplied -Force
if ($ApplyToUi) {
$control.IsChecked = $applied
$control.IsEnabled = -not ($applied -and $disableWhenApplied)
Add-Member -InputObject $control -MemberType NoteProperty -Name 'InitialState' -Value $applied -Force
}
}
elseif ($control -is [System.Windows.Controls.ComboBox] -and $mapping.Type -eq 'group') {
$groupId = $null
if ($controlName -match '^Group_(.+)Combo$') { $groupId = $matches[1] }
$activeIndex = 0
if ($groupId -and $groupMap.ContainsKey($groupId)) {
try { $activeIndex = Get-CurrentGroupActiveIndex -Group $groupMap[$groupId] } catch {}
}
Add-Member -InputObject $control -MemberType NoteProperty -Name 'SystemIndex' -Value $activeIndex -Force
if ($ApplyToUi) {
$control.SelectedIndex = $activeIndex
Add-Member -InputObject $control -MemberType NoteProperty -Name 'InitialIndex' -Value $activeIndex -Force
}
}
}
}
# Reads current registry state and sets each tweak control to reflect whether that tweak is
# currently applied. Also stores the initial state on each control as a NoteProperty so the
# apply handler can detect which controls actually changed.
function LoadCurrentTweakStateIntoUI {
RefreshCurrentTweakSystemState -ApplyToUi:$true
}
# Helper function to load apps and populate the app list panel # Helper function to load apps and populate the app list panel
function script:LoadAppsWithList($listOfApps) { function script:LoadAppsWithList($listOfApps) {
$script:MainWindowLastSelectedCheckbox = $null $script:MainWindowLastSelectedCheckbox = $null
@@ -1298,6 +1372,77 @@ function Show-MainWindow {
$col0 = $window.FindName('Column0Panel') $col0 = $window.FindName('Column0Panel')
$col1 = $window.FindName('Column1Panel') $col1 = $window.FindName('Column1Panel')
$col2 = $window.FindName('Column2Panel') $col2 = $window.FindName('Column2Panel')
$ShowCurrentlyAppliedTweaksCheckBox = $window.FindName('ShowCurrentlyAppliedTweaksCheckBox')
# Loads the currently applied tweaks from registry state into UI controls.
# When checkbox is checked: sets controls to their currently applied state
# When checkbox is unchecked: clears all control selections
function ResetTweaksToSystemState {
param ([bool]$loadSystemState)
if (-not $script:UiControlMappings) { return }
foreach ($controlName in $script:UiControlMappings.Keys) {
$control = $window.FindName($controlName)
if (-not $control) { continue }
if ($control -is [System.Windows.Controls.CheckBox]) {
if ($loadSystemState) {
# Set checkbox to the currently applied state from registry
$applied = if ($null -ne $control.PSObject.Properties['SystemState']) { [bool]$control.SystemState } else { $false }
$disableWhenApplied = $null -ne $control.PSObject.Properties['DisableWhenApplied'] -and [bool]$control.DisableWhenApplied
$control.IsChecked = $applied
$control.IsEnabled = -not ($applied -and $disableWhenApplied)
Add-Member -InputObject $control -MemberType NoteProperty -Name 'InitialState' -Value $applied -Force
} else {
# Clear the checkbox
$control.IsChecked = $false
$control.IsEnabled = $true
Add-Member -InputObject $control -MemberType NoteProperty -Name 'InitialState' -Value $false -Force
}
}
elseif ($control -is [System.Windows.Controls.ComboBox]) {
if ($loadSystemState) {
# Set combobox to the currently applied state from registry
$idx = if ($null -ne $control.PSObject.Properties['SystemIndex']) { [int]$control.SystemIndex } else { 0 }
$control.SelectedIndex = $idx
Add-Member -InputObject $control -MemberType NoteProperty -Name 'InitialIndex' -Value $idx -Force
} else {
# Reset to first item (No Change)
$control.SelectedIndex = 0
Add-Member -InputObject $control -MemberType NoteProperty -Name 'InitialIndex' -Value 0 -Force
}
}
}
}
$script:SuppressAppliedTweaksUserSync = $false
function UpdateAppliedTweaksUserModeState {
$showAppliedTweaksMode = ($ShowCurrentlyAppliedTweaksCheckBox -and $ShowCurrentlyAppliedTweaksCheckBox.IsChecked -eq $true)
if ($showAppliedTweaksMode) {
if ($userSelectionCombo.SelectedIndex -ne 0 -and -not $script:SuppressAppliedTweaksUserSync) {
$script:SuppressAppliedTweaksUserSync = $true
try {
$userSelectionCombo.SelectedIndex = 0
}
finally {
$script:SuppressAppliedTweaksUserSync = $false
}
}
$userSelectionCombo.IsEnabled = $false
return
}
if ($script:Params.ContainsKey('Sysprep') -or $script:Params.ContainsKey('User')) {
$userSelectionCombo.IsEnabled = $false
return
}
$userSelectionCombo.IsEnabled = $true
}
function UpdateTweaksResponsiveColumns { function UpdateTweaksResponsiveColumns {
if (-not $tweaksGrid -or -not $col0 -or -not $col1 -or -not $col2) { return } if (-not $tweaksGrid -or -not $col0 -or -not $col1 -or -not $col2) { return }
@@ -1440,6 +1585,12 @@ function Show-MainWindow {
} }
}) })
# Only show changed settings checkbox
if ($ShowCurrentlyAppliedTweaksCheckBox) {
$ShowCurrentlyAppliedTweaksCheckBox.Add_Checked({ ResetTweaksToSystemState -loadSystemState $true; UpdateAppliedTweaksUserModeState })
$ShowCurrentlyAppliedTweaksCheckBox.Add_Unchecked({ ResetTweaksToSystemState -loadSystemState $false; UpdateAppliedTweaksUserModeState })
}
# Add Ctrl+F keyboard shortcut to focus search box on current tab # Add Ctrl+F keyboard shortcut to focus search box on current tab
$window.Add_KeyDown({ $window.Add_KeyDown({
param($sourceControl, $e) param($sourceControl, $e)
@@ -1538,6 +1689,17 @@ function Show-MainWindow {
# Update user selection description and show/hide other user panel # Update user selection description and show/hide other user panel
$userSelectionCombo.Add_SelectionChanged({ $userSelectionCombo.Add_SelectionChanged({
if ($ShowCurrentlyAppliedTweaksCheckBox -and $ShowCurrentlyAppliedTweaksCheckBox.IsChecked -eq $true -and $userSelectionCombo.SelectedIndex -ne 0 -and -not $script:SuppressAppliedTweaksUserSync) {
$script:SuppressAppliedTweaksUserSync = $true
try {
$userSelectionCombo.SelectedIndex = 0
}
finally {
$script:SuppressAppliedTweaksUserSync = $false
}
return
}
switch ($userSelectionCombo.SelectedIndex) { switch ($userSelectionCombo.SelectedIndex) {
0 { 0 {
$userSelectionDescription.Text = "Changes will be applied to the currently logged-in user profile." $userSelectionDescription.Text = "Changes will be applied to the currently logged-in user profile."
@@ -1571,6 +1733,7 @@ function Show-MainWindow {
# Keep enabled/disabled state in sync with both app selection and user mode. # Keep enabled/disabled state in sync with both app selection and user mode.
UpdateAppSelectionStatus UpdateAppSelectionStatus
UpdateAppliedTweaksUserModeState
}) })
# Helper function to update app removal scope description # Helper function to update app removal scope description
@@ -1627,8 +1790,102 @@ function Show-MainWindow {
return $false return $false
} }
function Get-FeatureLabel {
param(
[string]$FeatureId,
$FallbackLabel = $null
)
$label = $script:FeatureLabelLookup[$FeatureId]
if (-not [string]::IsNullOrWhiteSpace([string]$label)) {
return [string]$label
}
if (-not [string]::IsNullOrWhiteSpace([string]$FallbackLabel)) {
return [string]$FallbackLabel
}
return [string]$FeatureId
}
function Get-PendingTweakActions {
param(
[bool]$ShowAppliedTweaksMode
)
$actions = New-Object System.Collections.Generic.List[object]
if (-not $script:UiControlMappings) {
return @($actions.ToArray())
}
foreach ($mappingKey in $script:UiControlMappings.Keys) {
$control = $window.FindName($mappingKey)
if (-not $control) { continue }
$mapping = $script:UiControlMappings[$mappingKey]
if ($control -is [System.Windows.Controls.CheckBox] -and $mapping.Type -eq 'feature') {
$wasApplied = $false
if ($ShowAppliedTweaksMode -and $null -ne $control.PSObject.Properties['SystemState']) {
$wasApplied = [bool]$control.SystemState
}
elseif ($null -ne $control.PSObject.Properties['InitialState']) {
$wasApplied = [bool]$control.InitialState
}
elseif ($null -ne $control.PSObject.Properties['SystemState']) {
$wasApplied = [bool]$control.SystemState
}
$isNowChecked = $control.IsChecked -eq $true
if (-not $wasApplied -and $isNowChecked) {
$actions.Add([PSCustomObject]@{
Action = 'Apply'
FeatureId = [string]$mapping.FeatureId
Label = (Get-FeatureLabel -FeatureId $mapping.FeatureId -FallbackLabel $mapping.Label)
})
}
elseif ($wasApplied -and -not $isNowChecked) {
$actions.Add([PSCustomObject]@{
Action = 'Undo'
FeatureId = [string]$mapping.FeatureId
Label = (Get-FeatureLabel -FeatureId $mapping.FeatureId -FallbackLabel $mapping.Label)
})
}
}
elseif ($control -is [System.Windows.Controls.ComboBox] -and $mapping.Type -eq 'group') {
$wasIndex = 0
if ($ShowAppliedTweaksMode -and $null -ne $control.PSObject.Properties['SystemIndex']) {
$wasIndex = [int]$control.SystemIndex
}
elseif ($null -ne $control.PSObject.Properties['InitialIndex']) {
$wasIndex = [int]$control.InitialIndex
}
elseif ($null -ne $control.PSObject.Properties['SystemIndex']) {
$wasIndex = [int]$control.SystemIndex
}
$isNowIndex = $control.SelectedIndex
if ($wasIndex -eq $isNowIndex) { continue }
if ($isNowIndex -gt 0 -and $isNowIndex -le $mapping.Values.Count) {
$selectedValue = $mapping.Values[$isNowIndex - 1]
foreach ($fid in $selectedValue.FeatureIds) {
$actions.Add([PSCustomObject]@{
Action = 'Apply'
FeatureId = [string]$fid
Label = (Get-FeatureLabel -FeatureId $fid)
})
}
}
}
}
return @($actions.ToArray())
}
function GenerateOverview { function GenerateOverview {
$changesList = @() $changesList = @()
$showAppliedTweaksMode = ($ShowCurrentlyAppliedTweaksCheckBox -and $ShowCurrentlyAppliedTweaksCheckBox.IsChecked -eq $true)
# Collect selected apps # Collect selected apps
$selectedAppsCount = 0 $selectedAppsCount = 0
@@ -1643,36 +1900,12 @@ function Show-MainWindow {
UpdateAppSelectionStatus UpdateAppSelectionStatus
# Collect all ComboBox/CheckBox selections from dynamically created controls foreach ($tweakAction in @(Get-PendingTweakActions -ShowAppliedTweaksMode:$showAppliedTweaksMode)) {
if ($script:UiControlMappings) { if ($tweakAction.Action -eq 'Undo') {
foreach ($mappingKey in $script:UiControlMappings.Keys) { $changesList += "Undo: $($tweakAction.Label)"
$control = $window.FindName($mappingKey) }
$isSelected = $false else {
$changesList += $tweakAction.Label
# Check if it's a checkbox or combobox
if ($control -is [System.Windows.Controls.CheckBox]) {
$isSelected = $control.IsChecked -eq $true
}
elseif ($control -is [System.Windows.Controls.ComboBox]) {
$isSelected = $control.SelectedIndex -gt 0
}
if ($control -and $isSelected) {
$mapping = $script:UiControlMappings[$mappingKey]
if ($mapping.Type -eq 'group') {
# For combobox: SelectedIndex 0 = No Change, so subtract 1 to index into Values
$selectedValue = $mapping.Values[$control.SelectedIndex - 1]
foreach ($fid in $selectedValue.FeatureIds) {
$label = $script:FeatureLabelLookup[$fid]
if ($label) { $changesList += $label }
}
}
elseif ($mapping.Type -eq 'feature') {
$label = $script:FeatureLabelLookup[$mapping.FeatureId]
if (-not $label) { $label = $mapping.Label }
$changesList += $label
}
}
} }
} }
@@ -1717,6 +1950,10 @@ function Show-MainWindow {
# Handle Home Default Mode button - apply defaults and navigate directly to overview # Handle Home Default Mode button - apply defaults and navigate directly to overview
$homeDefaultModeBtn = $window.FindName('HomeDefaultModeBtn') $homeDefaultModeBtn = $window.FindName('HomeDefaultModeBtn')
$homeDefaultModeBtn.Add_Click({ $homeDefaultModeBtn.Add_Click({
if ($ShowCurrentlyAppliedTweaksCheckBox) {
$ShowCurrentlyAppliedTweaksCheckBox.IsChecked = $false
}
# Load and apply default settings # Load and apply default settings
$defaultsJson = LoadJsonFile -filePath $script:DefaultSettingsFilePath -expectedVersion "1.0" $defaultsJson = LoadJsonFile -filePath $script:DefaultSettingsFilePath -expectedVersion "1.0"
if ($defaultsJson) { if ($defaultsJson) {
@@ -1763,6 +2000,9 @@ function Show-MainWindow {
Hide-Bubble -Immediate Hide-Bubble -Immediate
$showAppliedTweaksMode = ($ShowCurrentlyAppliedTweaksCheckBox -and $ShowCurrentlyAppliedTweaksCheckBox.IsChecked -eq $true)
$selectedForwardFeatureIds = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
# App Removal - collect selected apps from integrated UI # App Removal - collect selected apps from integrated UI
$selectedApps = @() $selectedApps = @()
foreach ($child in $appsPanel.Children) { foreach ($child in $appsPanel.Children) {
@@ -1771,6 +2011,7 @@ function Show-MainWindow {
} }
} }
$selectedApps = @($selectedApps | Where-Object { $_ } | Select-Object -Unique) $selectedApps = @($selectedApps | Where-Object { $_ } | Select-Object -Unique)
$hasAppSelection = ($selectedApps.Count -gt 0)
if ($selectedApps.Count -gt 0) { if ($selectedApps.Count -gt 0) {
# Check if Microsoft Store is selected # Check if Microsoft Store is selected
@@ -1803,56 +2044,18 @@ function Show-MainWindow {
} }
} }
# Apply dynamic tweaks selections # Apply dynamic tweaks - only controls that changed from their current baseline state
if ($script:UiControlMappings) { foreach ($tweakAction in @(Get-PendingTweakActions -ShowAppliedTweaksMode:$showAppliedTweaksMode)) {
foreach ($mappingKey in $script:UiControlMappings.Keys) { if ($tweakAction.Action -eq 'Apply') {
$control = $window.FindName($mappingKey) AddParameter $tweakAction.FeatureId
$isSelected = $false $null = $selectedForwardFeatureIds.Add([string]$tweakAction.FeatureId)
$selectedIndex = 0 continue
# Check if it's a checkbox or combobox
if ($control -is [System.Windows.Controls.CheckBox]) {
$isSelected = $control.IsChecked -eq $true
$selectedIndex = if ($isSelected) { 1 } else { 0 }
}
elseif ($control -is [System.Windows.Controls.ComboBox]) {
$isSelected = $control.SelectedIndex -gt 0
$selectedIndex = $control.SelectedIndex
}
if ($control -and $isSelected) {
$mapping = $script:UiControlMappings[$mappingKey]
if ($mapping.Type -eq 'group') {
if ($selectedIndex -gt 0 -and $selectedIndex -le $mapping.Values.Count) {
$selectedValue = $mapping.Values[$selectedIndex - 1]
foreach ($fid in $selectedValue.FeatureIds) {
AddParameter $fid
}
}
}
elseif ($mapping.Type -eq 'feature') {
AddParameter $mapping.FeatureId
}
}
} }
$script:UndoParams[[string]$tweakAction.FeatureId] = $true
} }
$controlParamsCount = 0 if (-not $hasAppSelection -and $selectedForwardFeatureIds.Count -eq 0 -and $script:UndoParams.Count -eq 0) {
foreach ($Param in $script:ControlParams) {
if ($script:Params.ContainsKey($Param)) {
$controlParamsCount++
}
}
# Check if any changes were selected
$totalChanges = $script:Params.Count - $controlParamsCount
# Apps parameter does not count as a change itself
if ($script:Params.ContainsKey('Apps')) {
$totalChanges = $totalChanges - 1
}
if ($totalChanges -eq 0) {
Show-MessageBox -Message 'No changes have been selected, please select at least one option to proceed.' -Title 'No Changes Selected' -Button 'OK' -Icon 'Information' Show-MessageBox -Message 'No changes have been selected, please select at least one option to proceed.' -Title 'No Changes Selected' -Button 'OK' -Icon 'Information'
return return
} }
@@ -1894,6 +2097,7 @@ function Show-MainWindow {
# Initialize UI elements on window load # Initialize UI elements on window load
$window.Add_Loaded({ $window.Add_Loaded({
BuildDynamicTweaks BuildDynamicTweaks
LoadCurrentTweakStateIntoUI
UpdateTweaksResponsiveColumns UpdateTweaksResponsiveColumns
RefreshTweakPresetSources -defaultSettingsJson $defaultsJson -lastUsedSettingsJson $lastUsedSettingsJson RefreshTweakPresetSources -defaultSettingsJson $defaultsJson -lastUsedSettingsJson $lastUsedSettingsJson
RegisterTweakPresetControlStateHandlers RegisterTweakPresetControlStateHandlers
@@ -1928,6 +2132,7 @@ function Show-MainWindow {
$otherUsernameTextBox.IsEnabled = $false $otherUsernameTextBox.IsEnabled = $false
} }
UpdateAppliedTweaksUserModeState
UpdateNavigationButtons UpdateNavigationButtons
}) })
@@ -2224,6 +2429,10 @@ function Show-MainWindow {
# Clear All Tweaks button # Clear All Tweaks button
$clearAllTweaksBtn = $window.FindName('ClearAllTweaksBtn') $clearAllTweaksBtn = $window.FindName('ClearAllTweaksBtn')
$clearAllTweaksBtn.Add_Click({ $clearAllTweaksBtn.Add_Click({
if ($ShowCurrentlyAppliedTweaksCheckBox -and $ShowCurrentlyAppliedTweaksCheckBox.IsChecked -eq $true) {
# Keep the toggle state aligned with the cleared UI selection state.
$ShowCurrentlyAppliedTweaksCheckBox.IsChecked = $false
}
ClearTweakSelections ClearTweakSelections
UpdateTweakPresetStates UpdateTweakPresetStates
}) })

View File

@@ -70,6 +70,9 @@ function Show-RestoreBackupDialog {
$backupCreatedText = $window.FindName('BackupCreatedText') $backupCreatedText = $window.FindName('BackupCreatedText')
$backupTargetText = $window.FindName('BackupTargetText') $backupTargetText = $window.FindName('BackupTargetText')
$featuresItemsControl = $window.FindName('FeaturesItemsControl') $featuresItemsControl = $window.FindName('FeaturesItemsControl')
$reappliedSeparator = $window.FindName('ReappliedSeparator')
$reappliedPanel = $window.FindName('ReappliedPanel')
$reappliedFeaturesItemsControl = $window.FindName('ReappliedFeaturesItemsControl')
$nonRevertibleSeparator = $window.FindName('NonRevertibleSeparator') $nonRevertibleSeparator = $window.FindName('NonRevertibleSeparator')
$nonRevertiblePanel = $window.FindName('NonRevertiblePanel') $nonRevertiblePanel = $window.FindName('NonRevertiblePanel')
$nonRevertibleFeaturesItemsControl = $window.FindName('NonRevertibleFeaturesItemsControl') $nonRevertibleFeaturesItemsControl = $window.FindName('NonRevertibleFeaturesItemsControl')
@@ -119,6 +122,8 @@ function Show-RestoreBackupDialog {
$overviewFeaturesSection.Visibility = 'Collapsed' $overviewFeaturesSection.Visibility = 'Collapsed'
$overviewSummaryText.Visibility = 'Visible' $overviewSummaryText.Visibility = 'Visible'
$reappliedSeparator.Visibility = 'Collapsed'
$reappliedPanel.Visibility = 'Collapsed'
$nonRevertibleSeparator.Visibility = 'Collapsed' $nonRevertibleSeparator.Visibility = 'Collapsed'
$nonRevertiblePanel.Visibility = 'Collapsed' $nonRevertiblePanel.Visibility = 'Collapsed'
$introInfoPanel.Visibility = 'Collapsed' $introInfoPanel.Visibility = 'Collapsed'
@@ -215,13 +220,33 @@ function Show-RestoreBackupDialog {
} }
} }
$selectedFeatureIds = Get-SelectedFeatureIdsFromBackup -SelectedBackup $SelectedBackup $selectedForwardFeatureIds = @(Get-SelectedForwardFeatureIdsFromBackup -SelectedBackup $SelectedBackup)
$featureLists = Get-RestoreBackupFeatureLists -SelectedFeatureIds $selectedFeatureIds -Features $script:Features $selectedUndoFeatureIds = @(Get-SelectedUndoFeatureIdsFromBackup -SelectedBackup $SelectedBackup)
$revertibleFeaturesList = @($featureLists.Revertible)
$nonRevertibleFeaturesList = @($featureLists.NonRevertible)
Write-Host "Backup overview prepared. Revertible=$($revertibleFeaturesList.Count), NonRevertible=$($nonRevertibleFeaturesList.Count)"
if ($revertibleFeaturesList.Count -eq 0) { $seenForwardFeatureIds = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
foreach ($featureId in $selectedForwardFeatureIds) {
[void]$seenForwardFeatureIds.Add([string]$featureId)
}
$filteredUndoFeatureIds = New-Object System.Collections.Generic.List[string]
foreach ($featureId in $selectedUndoFeatureIds) {
if ($seenForwardFeatureIds.Contains([string]$featureId)) {
continue
}
$filteredUndoFeatureIds.Add([string]$featureId)
}
$forwardFeatureLists = Get-RestoreBackupFeatureLists -SelectedFeatureIds $selectedForwardFeatureIds -Features $script:Features
$undoFeatureLists = Get-RestoreBackupFeatureLists -SelectedFeatureIds @($filteredUndoFeatureIds.ToArray()) -Features $script:Features
$combinedFeatureLists = Get-RestoreBackupFeatureLists -SelectedFeatureIds (Get-SelectedFeatureIdsFromBackup -SelectedBackup $SelectedBackup) -Features $script:Features
$revertibleFeaturesList = @($forwardFeatureLists.Revertible)
$reappliedFeaturesList = @($undoFeatureLists.Revertible)
$nonRevertibleFeaturesList = @($combinedFeatureLists.NonRevertible)
Write-Host "Backup overview prepared. Reverted=$($revertibleFeaturesList.Count), ReApplied=$($reappliedFeaturesList.Count), NonRevertible=$($nonRevertibleFeaturesList.Count)"
if ($revertibleFeaturesList.Count -eq 0 -and $reappliedFeaturesList.Count -eq 0) {
throw 'The selected backup does not contain any changes that can be restored.' throw 'The selected backup does not contain any changes that can be restored.'
} }
@@ -229,13 +254,16 @@ function Show-RestoreBackupDialog {
$backupCreatedText.Text = $createdText $backupCreatedText.Text = $createdText
$backupTargetText.Text = GetFriendlyRegistryBackupTarget -Target ([string]$SelectedBackup.Target) $backupTargetText.Text = GetFriendlyRegistryBackupTarget -Target ([string]$SelectedBackup.Target)
$featuresItemsControl.ItemsSource = $revertibleFeaturesList $featuresItemsControl.ItemsSource = $revertibleFeaturesList
$overviewFeaturesSection.Visibility = 'Visible' $overviewFeaturesSection.Visibility = if ($revertibleFeaturesList.Count -gt 0) { 'Visible' } else { 'Collapsed' }
$reappliedFeaturesItemsControl.ItemsSource = $reappliedFeaturesList
if ($reappliedFeaturesList.Count -gt 0) { $reappliedPanel.Visibility = 'Visible' } else { $reappliedPanel.Visibility = 'Collapsed' }
if ($revertibleFeaturesList.Count -gt 0 -and $reappliedFeaturesList.Count -gt 0) { $reappliedSeparator.Visibility = 'Visible' } else { $reappliedSeparator.Visibility = 'Collapsed' }
$overviewSummaryText.Visibility = 'Collapsed' $overviewSummaryText.Visibility = 'Collapsed'
$nonRevertibleFeaturesItemsControl.ItemsSource = $nonRevertibleFeaturesList $nonRevertibleFeaturesItemsControl.ItemsSource = $nonRevertibleFeaturesList
$hasNonRevertibleItems = ($nonRevertibleFeaturesList.Count -gt 0) $hasNonRevertibleItems = ($nonRevertibleFeaturesList.Count -gt 0)
if ($hasNonRevertibleItems) { $nonRevertiblePanel.Visibility = 'Visible' } else { $nonRevertiblePanel.Visibility = 'Collapsed' } if ($hasNonRevertibleItems) { $nonRevertiblePanel.Visibility = 'Visible' } else { $nonRevertiblePanel.Visibility = 'Collapsed' }
if ($hasNonRevertibleItems) { $nonRevertibleSeparator.Visibility = 'Visible' } else { $nonRevertibleSeparator.Visibility = 'Collapsed' } if ($hasNonRevertibleItems -and ($revertibleFeaturesList.Count -gt 0 -or $reappliedFeaturesList.Count -gt 0)) { $nonRevertibleSeparator.Visibility = 'Visible' } else { $nonRevertibleSeparator.Visibility = 'Collapsed' }
$introInfoPanel.Visibility = 'Collapsed' $introInfoPanel.Visibility = 'Collapsed'
$overviewPanel.Visibility = 'Visible' $overviewPanel.Visibility = 'Visible'

View File

@@ -7,10 +7,15 @@ function Show-RestoreBackupWindow {
try { try {
Write-Host 'Opening restore backup dialog.' Write-Host 'Opening restore backup dialog.'
$restoreResult = [PSCustomObject]@{
RestoredRegistry = $false
RestoredStartMenu = $false
}
$dialogResult = Show-RestoreBackupDialog -Owner $Owner $dialogResult = Show-RestoreBackupDialog -Owner $Owner
if (-not $dialogResult -or $dialogResult.Result -eq 'Cancel') { if (-not $dialogResult -or $dialogResult.Result -eq 'Cancel') {
Write-Host 'Restore canceled by user.' Write-Host 'Restore canceled by user.'
return return $restoreResult
} }
$successMessage = $null $successMessage = $null
@@ -24,7 +29,8 @@ function Show-RestoreBackupWindow {
Write-Host "User confirmed registry restore for $($backup.Target)." Write-Host "User confirmed registry restore for $($backup.Target)."
Restore-RegistryBackupState -Backup $backup Restore-RegistryBackupState -Backup $backup
$successMessage = 'Registry backup restored successfully. Please restart your computer for all changes to take effect.' $restoreResult.RestoredRegistry = $true
$successMessage = 'Registry backup restored successfully. Some changes may require a restart to take effect.'
} }
elseif ($dialogResult.Result -eq 'RestoreStartMenu') { elseif ($dialogResult.Result -eq 'RestoreStartMenu') {
$scope = $dialogResult.StartMenuScope $scope = $dialogResult.StartMenuScope
@@ -69,6 +75,8 @@ function Show-RestoreBackupWindow {
$successMessage = "The Start Menu backup was successfully restored for the current user. The changes will apply the next time you sign in." $successMessage = "The Start Menu backup was successfully restored for the current user. The changes will apply the next time you sign in."
} }
} }
$restoreResult.RestoredStartMenu = $true
} }
if ($warningMessage) { if ($warningMessage) {
@@ -79,10 +87,16 @@ function Show-RestoreBackupWindow {
Write-Host "$successMessage" Write-Host "$successMessage"
Show-MessageBox -Title 'Backup Restored' -Message $successMessage -Icon Success Show-MessageBox -Title 'Backup Restored' -Message $successMessage -Icon Success
} }
return $restoreResult
} }
catch { catch {
$errorMessage = if ($_.Exception.Message) { $_.Exception.Message } else { 'An unexpected error occurred.' } $errorMessage = if ($_.Exception.Message) { $_.Exception.Message } else { 'An unexpected error occurred.' }
Write-Error "Restore operation failed: $errorMessage" Write-Error "Restore operation failed: $errorMessage"
Show-MessageBox -Title 'Error' -Message "Restore failed: $errorMessage" -Icon Error Show-MessageBox -Title 'Error' -Message "Restore failed: $errorMessage" -Icon Error
return [PSCustomObject]@{
RestoredRegistry = $false
RestoredStartMenu = $false
}
} }
} }

View File

@@ -136,12 +136,12 @@ catch {
Exit Exit
} }
Write-Output "" # Remove old script folder if it exists, but keep configs, logs and backups
Write-Output "> Cleaning up old Win11Debloat folder..."
# Remove old script folder if it exists, but keep config and log files
if (Test-Path $tempWorkPath) { if (Test-Path $tempWorkPath) {
Get-ChildItem -Path $tempWorkPath -Exclude CustomAppsList,LastUsedSettings.json,Win11Debloat.log,Config,Logs,Backups | Remove-Item -Recurse -Force Write-Output ""
Write-Output "> Cleaning up old Win11Debloat folder..."
Get-ChildItem -Path $tempWorkPath -Exclude Config,Logs,Backups | Remove-Item -Recurse -Force
} }
$configDir = Join-Path $tempWorkPath 'Config' $configDir = Join-Path $tempWorkPath 'Config'
@@ -149,6 +149,9 @@ $backupDir = Join-Path $tempWorkPath 'ConfigOld'
# Temporarily move existing config files if they exist to prevent them from being overwritten by the new script files, will be moved back after the new script is unpacked # Temporarily move existing config files if they exist to prevent them from being overwritten by the new script files, will be moved back after the new script is unpacked
if (Test-Path "$configDir") { if (Test-Path "$configDir") {
Write-Output ""
Write-Output "> Backing up existing config files..."
New-Item -ItemType Directory -Path "$backupDir" -Force | Out-Null New-Item -ItemType Directory -Path "$backupDir" -Force | Out-Null
$filesToKeep = @( $filesToKeep = @(
@@ -179,6 +182,9 @@ if (Test-Path "$backupDir") {
New-Item -ItemType Directory -Path "$configDir" -Force | Out-Null New-Item -ItemType Directory -Path "$configDir" -Force | Out-Null
} }
Write-Output ""
Write-Output "> Restoring existing config files..."
Get-ChildItem -Path "$backupDir" -Recurse | Move-Item -Destination "$configDir" Get-ChildItem -Path "$backupDir" -Recurse | Move-Item -Destination "$configDir"
Remove-Item "$backupDir" -Recurse -Force Remove-Item "$backupDir" -Recurse -Force
} }
@@ -219,13 +225,13 @@ if ($null -ne $debloatProcess) {
$debloatProcess.WaitForExit() $debloatProcess.WaitForExit()
} }
# Remove all remaining script files, except for CustomAppsList and LastUsedSettings.json files # Remove all remaining script files, except for configs, logs and backups
if (Test-Path $tempWorkPath) { if (Test-Path $tempWorkPath) {
Write-Output "" Write-Output ""
Write-Output "> Cleaning up..." Write-Output "> Cleaning up..."
# Cleanup, remove Win11Debloat directory # Cleanup, remove Win11Debloat directory
Get-ChildItem -Path $tempWorkPath -Exclude CustomAppsList,LastUsedSettings.json,Win11Debloat.log,Win11Debloat-Run.log,Config,Logs,Backups | Remove-Item -Recurse -Force Get-ChildItem -Path $tempWorkPath -Exclude Config,Logs,Backups | Remove-Item -Recurse -Force
} }
Write-Output "" Write-Output ""

View File

@@ -129,10 +129,13 @@ function Convert-RegValueData {
} }
if ($valueData -match '^"(?<value>.*)"$') { if ($valueData -match '^"(?<value>.*)"$') {
$stringValue = $matches.value
# Unescape registry string escape sequences
$stringValue = $stringValue -replace '\\"', '"' -replace '\\\\', '\'
return [PSCustomObject]@{ return [PSCustomObject]@{
OperationType = 'SetValue' OperationType = 'SetValue'
ValueType = 'String' ValueType = 'String'
ValueData = $matches.value ValueData = $stringValue
} }
} }

View File

@@ -67,14 +67,14 @@ function Get-RegistryRootKey {
function Get-RegistryFilePathForFeature { function Get-RegistryFilePathForFeature {
param( param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
$Feature, [string]$RegistryKey,
[switch]$UseSysprepRegFiles [switch]$UseSysprepRegFiles
) )
$useSysprepLayout = $UseSysprepRegFiles -or $script:Params.ContainsKey('Sysprep') -or $script:Params.ContainsKey('User') $useSysprepLayout = $UseSysprepRegFiles -or $script:Params.ContainsKey('Sysprep') -or $script:Params.ContainsKey('User')
if ($useSysprepLayout) { if ($useSysprepLayout) {
return Join-Path (Join-Path $script:RegfilesPath 'Sysprep') $Feature.RegistryKey return Join-Path (Join-Path $script:RegfilesPath 'Sysprep') $RegistryKey
} }
return Join-Path $script:RegfilesPath $Feature.RegistryKey return Join-Path $script:RegfilesPath $RegistryKey
} }

View File

@@ -293,6 +293,7 @@ if (-not $script:WingetInstalled -and -not $Silent) {
. "$PSScriptRoot/Scripts/CLI/PrintHeader.ps1" . "$PSScriptRoot/Scripts/CLI/PrintHeader.ps1"
# Features functions # Features functions
. "$PSScriptRoot/Scripts/Features/GetCurrentTweakState.ps1"
. "$PSScriptRoot/Scripts/Features/ExecuteChanges.ps1" . "$PSScriptRoot/Scripts/Features/ExecuteChanges.ps1"
. "$PSScriptRoot/Scripts/Features/CreateSystemRestorePoint.ps1" . "$PSScriptRoot/Scripts/Features/CreateSystemRestorePoint.ps1"
. "$PSScriptRoot/Scripts/Features/BackupRegistryFeatureSelection.ps1" . "$PSScriptRoot/Scripts/Features/BackupRegistryFeatureSelection.ps1"
@@ -373,6 +374,7 @@ $WinVersion = Get-ItemPropertyValue 'HKLM:\SOFTWARE\Microsoft\Windows NT\Current
$script:ModernStandbySupported = CheckModernStandbySupport $script:ModernStandbySupported = CheckModernStandbySupport
$script:Params = $PSBoundParameters $script:Params = $PSBoundParameters
$script:UndoParams = @{}
# Add default Apps parameter when RemoveApps is requested and Apps was not explicitly provided # Add default Apps parameter when RemoveApps is requested and Apps was not explicitly provided
if ((-not $script:Params.ContainsKey("Apps")) -and $script:Params.ContainsKey("RemoveApps")) { if ((-not $script:Params.ContainsKey("Apps")) -and $script:Params.ContainsKey("RemoveApps")) {