mirror of
https://github.com/Raphire/Win11Debloat.git
synced 2026-06-10 10:36:26 +00:00
Compare commits
27 Commits
2026.05.10
...
icon-test
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12f3ce401b | ||
|
|
924772c3a0 | ||
|
|
68248b4a04 | ||
|
|
1ed967b9d3 | ||
|
|
4332eaa833 | ||
|
|
9deeb295e7 | ||
|
|
f6ed6ac487 | ||
|
|
b920536be2 | ||
|
|
7273f29fea | ||
|
|
7381c29da2 | ||
|
|
3bed9cafbc | ||
|
|
6dbaac0513 | ||
|
|
6e63b34dbb | ||
|
|
3f763b01ab | ||
|
|
4109588e0f | ||
|
|
37872b2030 | ||
|
|
abfc5db2c3 | ||
|
|
1d828d6a78 | ||
|
|
4d9da4749b | ||
|
|
5cf9ac4082 | ||
|
|
924c192ca5 | ||
|
|
2a5cb986c9 | ||
|
|
66982ada28 | ||
|
|
489af33a8b | ||
|
|
51aa288dfd | ||
|
|
24a6f1bcf8 | ||
|
|
8ac664e45f |
2
.github/CONTRIBUTING.md
vendored
2
.github/CONTRIBUTING.md
vendored
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -366,7 +366,7 @@
|
|||||||
{
|
{
|
||||||
"FeatureId": "DisableTelemetry",
|
"FeatureId": "DisableTelemetry",
|
||||||
"Label": "Disable telemetry, tracking & targeted ads",
|
"Label": "Disable telemetry, tracking & targeted ads",
|
||||||
"ToolTip": "This setting disables telemetry, diagnostic data collection, activity history, app-launch tracking, targeted ads and more. It limits the data that is sent to Microsoft about your device and usage.",
|
"ToolTip": "This setting disables telemetry, diagnostic data collection, activity history, app-launch tracking, targeted ads and more. It limits the data that is sent to Microsoft about your device and usage. If you are a Windows Insider, updates may be blocked until optional diagnostic data collection is turned back on.",
|
||||||
"Category": "Privacy & Suggested Content",
|
"Category": "Privacy & Suggested Content",
|
||||||
"RegistryKey": "Disable_Telemetry.reg",
|
"RegistryKey": "Disable_Telemetry.reg",
|
||||||
"ApplyText": "Disabling telemetry, diagnostic data, activity history, app-launch tracking and targeted ads...",
|
"ApplyText": "Disabling telemetry, diagnostic data, activity history, app-launch tracking and targeted ads...",
|
||||||
@@ -601,28 +601,6 @@
|
|||||||
"MinVersion": 22621,
|
"MinVersion": 22621,
|
||||||
"MaxVersion": null
|
"MaxVersion": null
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"FeatureId": "DisableSearchHighlights",
|
|
||||||
"Label": "Disable Search Highlights in the taskbar search box",
|
|
||||||
"ToolTip": "This will turn off Search Highlights, which shows dynamically curated branded content and trending topics in the Windows search box on the taskbar.",
|
|
||||||
"Category": "Start Menu & Search",
|
|
||||||
"RegistryKey": "Disable_Search_Highlights.reg",
|
|
||||||
"ApplyText": "Disabling Search Highlights in the Windows search box...",
|
|
||||||
"RegistryUndoKey": "Enable_Search_Highlights.reg",
|
|
||||||
"MinVersion": 22621,
|
|
||||||
"MaxVersion": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"FeatureId": "DisableSearchHistory",
|
|
||||||
"Label": "Disable local Windows Search history",
|
|
||||||
"ToolTip": "This setting disables local search history in Windows Search. This does not affect web search history or the search history saved in Microsoft Edge.",
|
|
||||||
"Category": "Start Menu & Search",
|
|
||||||
"RegistryKey": "Disable_Search_History.reg",
|
|
||||||
"ApplyText": "Disabling search history...",
|
|
||||||
"RegistryUndoKey": "Enable_Search_History.reg",
|
|
||||||
"MinVersion": null,
|
|
||||||
"MaxVersion": null
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"FeatureId": "DisableSettings365Ads",
|
"FeatureId": "DisableSettings365Ads",
|
||||||
"Label": "Hide Microsoft 365 Copilot ads in Settings Home",
|
"Label": "Hide Microsoft 365 Copilot ads in Settings Home",
|
||||||
@@ -878,14 +856,15 @@
|
|||||||
{
|
{
|
||||||
"FeatureId": "DisableWidgets",
|
"FeatureId": "DisableWidgets",
|
||||||
"Label": "Disable widgets on the taskbar & lock screen",
|
"Label": "Disable widgets on the taskbar & lock screen",
|
||||||
"ToolTip": "This will disable the widgets features in Windows, including the widgets button on the taskbar and the widgets that can appear on the lock screen. This feature uses policies, which will lock down certain settings.",
|
"ToolTip": "This will disable the widgets features in Windows, including the widgets button on the taskbar and the widgets that can appear on the lock screen.",
|
||||||
"Category": "Taskbar",
|
"Category": "Taskbar",
|
||||||
"Priority": 4,
|
"Priority": 4,
|
||||||
"RegistryKey": "Disable_Widgets_Service.reg",
|
"RegistryKey": null,
|
||||||
"ApplyText": "Disabling widgets on the taskbar & lock screen...",
|
"ApplyText": null,
|
||||||
"RegistryUndoKey": "Enable_Widgets_Service.reg",
|
"RegistryUndoKey": null,
|
||||||
"MinVersion": null,
|
"MinVersion": null,
|
||||||
"MaxVersion": null
|
"MaxVersion": null,
|
||||||
|
"DisableWhenApplied": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"FeatureId": "HideChat",
|
"FeatureId": "HideChat",
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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>
|
||||||
|
|||||||
@@ -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="*"/>
|
||||||
|
|||||||
@@ -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) })
|
||||||
|
|||||||
@@ -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()
|
|
||||||
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]
|
foreach ($feature in $UndoRegistryFeatures) {
|
||||||
switch ($operation.OperationType) {
|
$regFilePath = Resolve-RegistryBackupUndoFilePath -Feature $feature
|
||||||
'DeleteKey' {
|
if ([string]::IsNullOrWhiteSpace($regFilePath)) {
|
||||||
$plan.IncludeSubKeys = $true
|
continue
|
||||||
$plan.CaptureAllValues = $true
|
|
||||||
}
|
}
|
||||||
'SetValue' {
|
|
||||||
if (-not $plan.CaptureAllValues) {
|
if (-not (Test-Path $regFilePath)) {
|
||||||
$null = $plan.ValueNames.Add([string]$operation.ValueName)
|
$undoKeyDescription = if (-not [string]::IsNullOrWhiteSpace([string]$feature.RegistryUndoKey)) {
|
||||||
}
|
[string]$feature.RegistryUndoKey
|
||||||
}
|
|
||||||
'DeleteValue' {
|
|
||||||
if (-not $plan.CaptureAllValues) {
|
|
||||||
$null = $plan.ValueNames.Add([string]$operation.ValueName)
|
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
[string]$feature.RegistryKey
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw "Unable to find registry undo file for backup: $undoKeyDescription ($regFilePath)"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach ($operation in @(Get-RegFileOperations -regFilePath $regFilePath)) {
|
||||||
|
if (-not $operation.KeyPath) { continue }
|
||||||
|
Add-RegistryPlanOperation -PlanMap $planMap -Operation $operation
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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) {
|
||||||
@@ -226,13 +281,20 @@ function Convert-RegistryValueToSnapshot {
|
|||||||
|
|
||||||
$valueKind = $RegistryKey.GetValueKind($ValueName)
|
$valueKind = $RegistryKey.GetValueKind($ValueName)
|
||||||
$value = $RegistryKey.GetValue($ValueName, $null, [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)
|
$value = $RegistryKey.GetValue($ValueName, $null, [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)
|
||||||
|
try {
|
||||||
$normalizedValue = switch ($valueKind) {
|
$normalizedValue = switch ($valueKind) {
|
||||||
([Microsoft.Win32.RegistryValueKind]::Binary) { @($value | ForEach-Object { [int]$_ }) }
|
([Microsoft.Win32.RegistryValueKind]::Binary) { @($value | ForEach-Object { [int]$_ }) }
|
||||||
([Microsoft.Win32.RegistryValueKind]::MultiString) { @($value) }
|
([Microsoft.Win32.RegistryValueKind]::MultiString) { @($value) }
|
||||||
([Microsoft.Win32.RegistryValueKind]::DWord) { [uint32]$value }
|
([Microsoft.Win32.RegistryValueKind]::DWord) { [BitConverter]::ToUInt32([BitConverter]::GetBytes([int32]$value), 0) }
|
||||||
([Microsoft.Win32.RegistryValueKind]::QWord) { [uint64]$value }
|
([Microsoft.Win32.RegistryValueKind]::QWord) { [BitConverter]::ToUInt64([BitConverter]::GetBytes([int64]$value), 0) }
|
||||||
default { if ($null -ne $value) { [string]$value } else { $null } }
|
default { if ($null -ne $value) { [string]$value } else { $null } }
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
$valueType = if ($null -ne $value) { $value.GetType().FullName } else { '<null>' }
|
||||||
|
$valueForLog = if ($null -eq $value) { '<null>' } elseif ($value -is [array]) { ($value -join ',') } else { [string]$value }
|
||||||
|
throw "Failed to normalize registry value for backup. Key='$($RegistryKey.Name)' Name='$ValueName' Kind='$valueKind' RawType='$valueType' RawValue='$valueForLog'. InnerError: $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
|
||||||
return @{
|
return @{
|
||||||
Name = $ValueName
|
Name = $ValueName
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ function CreateSystemRestorePoint {
|
|||||||
# In GUI mode, skip the prompt and just try to enable it
|
# In GUI mode, skip the prompt and just try to enable it
|
||||||
if ($script:GuiWindow -or $Silent -or $( Read-Host -Prompt "System restore is disabled, would you like to enable it and create a restore point? (y/n)") -eq 'y') {
|
if ($script:GuiWindow -or $Silent -or $( Read-Host -Prompt "System restore is disabled, would you like to enable it and create a restore point? (y/n)") -eq 'y') {
|
||||||
try {
|
try {
|
||||||
$enableResult = Invoke-NonBlocking -TimeoutSeconds 20 -ScriptBlock {
|
$enableResult = Invoke-NonBlocking -TimeoutSeconds 90 -ScriptBlock {
|
||||||
try {
|
try {
|
||||||
Enable-ComputerRestore -Drive "$env:SystemDrive"
|
Enable-ComputerRestore -Drive "$env:SystemDrive"
|
||||||
return $null
|
return $null
|
||||||
@@ -33,7 +33,7 @@ function CreateSystemRestorePoint {
|
|||||||
|
|
||||||
if (-not $failed) {
|
if (-not $failed) {
|
||||||
try {
|
try {
|
||||||
$result = Invoke-NonBlocking -TimeoutSeconds 20 -ScriptBlock {
|
$result = Invoke-NonBlocking -TimeoutSeconds 90 -ScriptBlock {
|
||||||
try {
|
try {
|
||||||
$recentRestorePoints = Get-ComputerRestorePoint | Where-Object { (Get-Date) - [System.Management.ManagementDateTimeConverter]::ToDateTime($_.CreationTime) -le (New-TimeSpan -Hours 24) }
|
$recentRestorePoints = Get-ComputerRestorePoint | Where-Object { (Get-Date) - [System.Management.ManagementDateTimeConverter]::ToDateTime($_.CreationTime) -le (New-TimeSpan -Hours 24) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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')
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -26,10 +78,6 @@ function ExecuteParameter {
|
|||||||
# Also remove the app package for Copilot
|
# Also remove the app package for Copilot
|
||||||
RemoveApps 'Microsoft.Copilot'
|
RemoveApps 'Microsoft.Copilot'
|
||||||
}
|
}
|
||||||
'DisableWidgets' {
|
|
||||||
# Also remove the app packages for Widgets
|
|
||||||
RemoveApps 'Microsoft.StartExperiencesApp','MicrosoftWindows.Client.WebExperience','Microsoft.WidgetsPlatformRuntime'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -86,6 +134,13 @@ function ExecuteParameter {
|
|||||||
RemoveApps $appsList
|
RemoveApps $appsList
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
'DisableWidgets' {
|
||||||
|
Write-Host "> Disabling widgets on the taskbar & lock screen..."
|
||||||
|
# Stop widgets related processes before removing the app packages to prevent potential issues
|
||||||
|
Get-Process *Widget* | Stop-Process
|
||||||
|
|
||||||
|
RemoveApps 'Microsoft.StartExperiencesApp','MicrosoftWindows.Client.WebExperience','Microsoft.WidgetsPlatformRuntime'
|
||||||
|
}
|
||||||
"EnableWindowsSandbox" {
|
"EnableWindowsSandbox" {
|
||||||
Write-Host "> Enabling Windows Sandbox..."
|
Write-Host "> Enabling Windows Sandbox..."
|
||||||
EnableWindowsFeature "Containers-DisposableClientVM"
|
EnableWindowsFeature "Containers-DisposableClientVM"
|
||||||
@@ -138,6 +193,8 @@ function ExecuteParameter {
|
|||||||
|
|
||||||
# Executes all selected parameters/features
|
# Executes all selected parameters/features
|
||||||
function ExecuteAllChanges {
|
function ExecuteAllChanges {
|
||||||
|
$script:RegistryImportFailures = 0
|
||||||
|
|
||||||
# Build list of actionable parameters (skip control params and data-only params)
|
# Build list of actionable parameters (skip control params and data-only params)
|
||||||
$actionableKeys = @()
|
$actionableKeys = @()
|
||||||
foreach ($paramKey in $script:Params.Keys) {
|
foreach ($paramKey in $script:Params.Keys) {
|
||||||
@@ -157,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
|
||||||
@@ -166,20 +230,31 @@ function ExecuteAllChanges {
|
|||||||
if ($hasRegistryBackedFeature) {
|
if ($hasRegistryBackedFeature) {
|
||||||
$currentStep++
|
$currentStep++
|
||||||
if ($script:ApplyProgressCallback) {
|
if ($script:ApplyProgressCallback) {
|
||||||
& $script:ApplyProgressCallback $currentStep $totalSteps "Creating registry backup"
|
& $script:ApplyProgressCallback $currentStep $totalSteps "Creating registry backup..."
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Host "> Creating registry backup..."
|
Write-Host "> Creating registry backup..."
|
||||||
New-RegistrySettingsBackup -ActionableKeys $actionableKeys | Out-Null
|
try {
|
||||||
|
$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 {
|
||||||
|
throw "Registry backup failed before applying changes. $($_.Exception.Message)"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Create restore point if requested (CLI only - GUI handles this separately)
|
# Create restore point if requested (CLI only - GUI handles this separately)
|
||||||
if ($script:Params.ContainsKey("CreateRestorePoint")) {
|
if ($script:Params.ContainsKey("CreateRestorePoint")) {
|
||||||
$currentStep++
|
$currentStep++
|
||||||
if ($script:ApplyProgressCallback) {
|
if ($script:ApplyProgressCallback) {
|
||||||
& $script:ApplyProgressCallback $currentStep $totalSteps "Creating system restore point"
|
& $script:ApplyProgressCallback $currentStep $totalSteps "Creating system restore point, this may take a moment..."
|
||||||
}
|
}
|
||||||
Write-Host "> Attempting to create a system restore point..."
|
Write-Host "> Creating a system restore point..."
|
||||||
CreateSystemRestorePoint
|
CreateSystemRestorePoint
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
}
|
}
|
||||||
@@ -211,4 +286,29 @@ 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) {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "$($script:RegistryImportFailures) registry import change(s) failed. See output above for details." -ForegroundColor Yellow
|
||||||
|
}
|
||||||
}
|
}
|
||||||
175
Scripts/Features/GetCurrentTweakState.ps1
Normal file
175
Scripts/Features/GetCurrentTweakState.ps1
Normal 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
|
||||||
|
}
|
||||||
@@ -8,33 +8,38 @@ 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")
|
||||||
$regFilePath = if ($usesOfflineHive) {
|
$regFilePath = Get-RegistryFilePathForFeature -RegistryKey $path
|
||||||
"$script:RegfilesPath\Sysprep\$path"
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
"$script:RegfilesPath\$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)"
|
||||||
|
$script:RegistryImportFailures++
|
||||||
Write-Host "Error: $errorMessage" -ForegroundColor Red
|
Write-Host "Error: $errorMessage" -ForegroundColor Red
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
throw $errorMessage
|
throw $errorMessage
|
||||||
}
|
}
|
||||||
|
|
||||||
# Reset exit code before running reg.exe for reliable success detection
|
$regResult = $null
|
||||||
$global:LASTEXITCODE = 0
|
$offlineHiveLoaded = $false
|
||||||
|
|
||||||
|
try {
|
||||||
if ($usesOfflineHive) {
|
if ($usesOfflineHive) {
|
||||||
# Sysprep targets Default user, User targets the specified user
|
# Sysprep targets Default user, User targets the specified user
|
||||||
$hiveDatPath = if ($script:Params.ContainsKey("Sysprep")) {
|
$targetUserName = if ($script:Params.ContainsKey("Sysprep")) { "Default" } else { $script:Params.Item("User") }
|
||||||
GetUserDirectory -userName "Default" -fileName "NTUSER.DAT"
|
$hiveDatPath = GetUserDirectory -userName $targetUserName -fileName "NTUSER.DAT"
|
||||||
} else {
|
|
||||||
GetUserDirectory -userName $script:Params.Item("User") -fileName "NTUSER.DAT"
|
$global:LASTEXITCODE = 0
|
||||||
|
reg load "HKU\Default" $hiveDatPath | Out-Null
|
||||||
|
$loadExitCode = $LASTEXITCODE
|
||||||
|
|
||||||
|
if ($loadExitCode -ne 0) {
|
||||||
|
throw "Failed importing registry file '$path'. Offline hive load failed: Failed to load user hive at '$hiveDatPath' (exit code: $loadExitCode)"
|
||||||
|
}
|
||||||
|
|
||||||
|
$offlineHiveLoaded = $true
|
||||||
}
|
}
|
||||||
|
|
||||||
$regResult = Invoke-NonBlocking -ScriptBlock {
|
$regResult = Invoke-NonBlocking -ScriptBlock {
|
||||||
param($hivePath, $targetRegFilePath)
|
param($targetRegFilePath)
|
||||||
$result = @{
|
$result = @{
|
||||||
Output = @()
|
Output = @()
|
||||||
ExitCode = 0
|
ExitCode = 0
|
||||||
@@ -43,13 +48,6 @@ function ImportRegistryFile {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
$global:LASTEXITCODE = 0
|
$global:LASTEXITCODE = 0
|
||||||
reg load "HKU\Default" $hivePath | Out-Null
|
|
||||||
$loadExitCode = $LASTEXITCODE
|
|
||||||
|
|
||||||
if ($loadExitCode -ne 0) {
|
|
||||||
throw "Failed to load user hive at '$hivePath' (exit code: $loadExitCode)"
|
|
||||||
}
|
|
||||||
|
|
||||||
$output = reg import $targetRegFilePath 2>&1
|
$output = reg import $targetRegFilePath 2>&1
|
||||||
$importExitCode = $LASTEXITCODE
|
$importExitCode = $LASTEXITCODE
|
||||||
|
|
||||||
@@ -66,27 +64,9 @@ function ImportRegistryFile {
|
|||||||
$result.Error = $_.Exception.Message
|
$result.Error = $_.Exception.Message
|
||||||
$result.ExitCode = if ($LASTEXITCODE -ne 0) { $LASTEXITCODE } else { 1 }
|
$result.ExitCode = if ($LASTEXITCODE -ne 0) { $LASTEXITCODE } else { 1 }
|
||||||
}
|
}
|
||||||
finally {
|
|
||||||
$global:LASTEXITCODE = 0
|
|
||||||
reg unload "HKU\Default" | Out-Null
|
|
||||||
$unloadExitCode = $LASTEXITCODE
|
|
||||||
if ($unloadExitCode -ne 0 -and -not $result.Error) {
|
|
||||||
$result.Error = "Failed to unload registry hive HKU\Default (exit code: $unloadExitCode)"
|
|
||||||
$result.ExitCode = $unloadExitCode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $result
|
return $result
|
||||||
} -ArgumentList @($hiveDatPath, $regFilePath)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$regResult = Invoke-NonBlocking -ScriptBlock {
|
|
||||||
param($targetRegFilePath)
|
|
||||||
$global:LASTEXITCODE = 0
|
|
||||||
$output = reg import $targetRegFilePath 2>&1
|
|
||||||
return @{ Output = @($output); ExitCode = $LASTEXITCODE; Error = $null }
|
|
||||||
} -ArgumentList $regFilePath
|
} -ArgumentList $regFilePath
|
||||||
}
|
|
||||||
|
|
||||||
$regOutput = @($regResult.Output)
|
$regOutput = @($regResult.Output)
|
||||||
$hasSuccess = ($regResult.ExitCode -eq 0) -and -not $regResult.Error
|
$hasSuccess = ($regResult.ExitCode -eq 0) -and -not $regResult.Error
|
||||||
@@ -107,11 +87,27 @@ function ImportRegistryFile {
|
|||||||
|
|
||||||
if (-not $hasSuccess) {
|
if (-not $hasSuccess) {
|
||||||
$details = if ($regResult.Error) { $regResult.Error } else { "Exit code: $($regResult.ExitCode)" }
|
$details = if ($regResult.Error) { $regResult.Error } else { "Exit code: $($regResult.ExitCode)" }
|
||||||
$errorMessage = "Failed importing registry file '$path'. $details"
|
Write-Warning "reg import failed for '$path'. Falling back to PowerShell registry writer. Details: $details"
|
||||||
Write-Host $errorMessage -ForegroundColor Red
|
Invoke-RegistryOperationsFromRegFile -RegFilePath $regFilePath
|
||||||
Write-Host ""
|
Write-Host "Fallback import succeeded for '$path'." -ForegroundColor Yellow
|
||||||
throw $errorMessage
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
}
|
}
|
||||||
|
catch {
|
||||||
|
$script:RegistryImportFailures++
|
||||||
|
Write-Host $_.Exception.Message -ForegroundColor Red
|
||||||
|
Write-Host ""
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
if ($offlineHiveLoaded) {
|
||||||
|
$global:LASTEXITCODE = 0
|
||||||
|
reg unload "HKU\Default" | Out-Null
|
||||||
|
$unloadExitCode = $LASTEXITCODE
|
||||||
|
|
||||||
|
if ($unloadExitCode -ne 0) {
|
||||||
|
Write-Warning "Failed to unload registry hive HKU\Default after importing '$path' (exit code: $unloadExitCode)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -157,8 +157,14 @@ function Convert-RegistryValueDataFromBackup {
|
|||||||
)
|
)
|
||||||
|
|
||||||
switch ($Kind) {
|
switch ($Kind) {
|
||||||
([Microsoft.Win32.RegistryValueKind]::DWord) { return [uint32]$Data }
|
([Microsoft.Win32.RegistryValueKind]::DWord) {
|
||||||
([Microsoft.Win32.RegistryValueKind]::QWord) { return [uint64]$Data }
|
$unsigned = [uint32]$Data
|
||||||
|
return [BitConverter]::ToInt32([BitConverter]::GetBytes($unsigned), 0)
|
||||||
|
}
|
||||||
|
([Microsoft.Win32.RegistryValueKind]::QWord) {
|
||||||
|
$unsigned = [uint64]$Data
|
||||||
|
return [BitConverter]::ToInt64([BitConverter]::GetBytes($unsigned), 0)
|
||||||
|
}
|
||||||
([Microsoft.Win32.RegistryValueKind]::MultiString) { return @($Data | ForEach-Object { [string]$_ }) }
|
([Microsoft.Win32.RegistryValueKind]::MultiString) { return @($Data | ForEach-Object { [string]$_ }) }
|
||||||
([Microsoft.Win32.RegistryValueKind]::Binary) {
|
([Microsoft.Win32.RegistryValueKind]::Binary) {
|
||||||
$bytes = Convert-BackupDataToByteArray -Data $Data
|
$bytes = Convert-BackupDataToByteArray -Data $Data
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,6 +124,8 @@ function Show-ApplyModal {
|
|||||||
try {
|
try {
|
||||||
ExecuteAllChanges
|
ExecuteAllChanges
|
||||||
|
|
||||||
|
$registryImportFailureCount = [int]$script:RegistryImportFailures
|
||||||
|
|
||||||
# Restart explorer if requested
|
# Restart explorer if requested
|
||||||
if ($RestartExplorer -and -not $script:CancelRequested) {
|
if ($RestartExplorer -and -not $script:CancelRequested) {
|
||||||
RestartExplorer
|
RestartExplorer
|
||||||
@@ -139,7 +141,7 @@ function Show-ApplyModal {
|
|||||||
Write-Host ""
|
Write-Host ""
|
||||||
if ($script:CancelRequested) {
|
if ($script:CancelRequested) {
|
||||||
Write-Host "Script execution was cancelled by the user. Some changes may not have been applied."
|
Write-Host "Script execution was cancelled by the user. Some changes may not have been applied."
|
||||||
} else {
|
} elseif ($registryImportFailureCount -eq 0) {
|
||||||
Write-Host "All changes have been applied successfully!"
|
Write-Host "All changes have been applied successfully!"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,6 +155,11 @@ function Show-ApplyModal {
|
|||||||
$script:ApplyCompletionIconEl.Foreground = [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#e8912d"))
|
$script:ApplyCompletionIconEl.Foreground = [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#e8912d"))
|
||||||
$script:ApplyCompletionTitleEl.Text = "Cancelled"
|
$script:ApplyCompletionTitleEl.Text = "Cancelled"
|
||||||
$script:ApplyCompletionMessageEl.Text = "Script execution was cancelled by the user."
|
$script:ApplyCompletionMessageEl.Text = "Script execution was cancelled by the user."
|
||||||
|
} elseif ($registryImportFailureCount -gt 0) {
|
||||||
|
$script:ApplyCompletionIconEl.Text = [char]0xE7BA
|
||||||
|
$script:ApplyCompletionIconEl.Foreground = [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#e8912d"))
|
||||||
|
$script:ApplyCompletionTitleEl.Text = "Changes Applied with Errors"
|
||||||
|
$script:ApplyCompletionMessageEl.Text = "$registryImportFailureCount registry change(s) failed. See console for details."
|
||||||
} else {
|
} else {
|
||||||
$script:ApplyCompletionTitleEl.Text = "Changes Applied"
|
$script:ApplyCompletionTitleEl.Text = "Changes Applied"
|
||||||
|
|
||||||
@@ -179,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!"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|
||||||
# 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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
$changesList += $tweakAction.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) {
|
$script:UndoParams[[string]$tweakAction.FeatureId] = $true
|
||||||
$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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$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
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|
||||||
@@ -255,7 +283,7 @@ function Show-RestoreBackupDialog {
|
|||||||
|
|
||||||
$openDialog = New-Object Microsoft.Win32.OpenFileDialog
|
$openDialog = New-Object Microsoft.Win32.OpenFileDialog
|
||||||
$openDialog.Title = 'Select Registry Backup File'
|
$openDialog.Title = 'Select Registry Backup File'
|
||||||
$openDialog.Filter = 'Registry backup (*.json)|*.json|All files (*.*)|*.*'
|
$openDialog.Filter = 'Registry backup (*.json)|*.json'
|
||||||
$openDialog.DefaultExt = '.json'
|
$openDialog.DefaultExt = '.json'
|
||||||
$openDialog.InitialDirectory = $script:RegistryBackupsPath
|
$openDialog.InitialDirectory = $script:RegistryBackupsPath
|
||||||
|
|
||||||
|
|||||||
@@ -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.'
|
$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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,12 +136,12 @@ catch {
|
|||||||
Exit
|
Exit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Remove old script folder if it exists, but keep configs, logs and backups
|
||||||
|
if (Test-Path $tempWorkPath) {
|
||||||
Write-Output ""
|
Write-Output ""
|
||||||
Write-Output "> Cleaning up old Win11Debloat folder..."
|
Write-Output "> Cleaning up old Win11Debloat folder..."
|
||||||
|
|
||||||
# Remove old script folder if it exists, but keep config and log files
|
Get-ChildItem -Path $tempWorkPath -Exclude Config,Logs,Backups | Remove-Item -Recurse -Force
|
||||||
if (Test-Path $tempWorkPath) {
|
|
||||||
Get-ChildItem -Path $tempWorkPath -Exclude CustomAppsList,LastUsedSettings.json,Win11Debloat.log,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 ""
|
||||||
|
|||||||
223
Scripts/Helpers/ApplyRegistryRegFile.ps1
Normal file
223
Scripts/Helpers/ApplyRegistryRegFile.ps1
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
function Get-NormalizedRegistryValueName {
|
||||||
|
param(
|
||||||
|
[AllowNull()]
|
||||||
|
$ValueName
|
||||||
|
)
|
||||||
|
|
||||||
|
if ([string]::IsNullOrEmpty([string]$ValueName)) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
return [string]$ValueName
|
||||||
|
}
|
||||||
|
|
||||||
|
function Convert-RegOperationToValueKind {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
$Operation
|
||||||
|
)
|
||||||
|
|
||||||
|
$valueName = if ([string]::IsNullOrEmpty([string]$Operation.ValueName)) { '' } else { [string]$Operation.ValueName }
|
||||||
|
$valueType = [string]$Operation.ValueType
|
||||||
|
$operationKeyPath = [string]$Operation.KeyPath
|
||||||
|
|
||||||
|
switch ($valueType) {
|
||||||
|
'DWord' {
|
||||||
|
$unsigned = [uint32]$Operation.ValueData
|
||||||
|
$value = [BitConverter]::ToInt32([BitConverter]::GetBytes($unsigned), 0)
|
||||||
|
return @{ Name = $valueName; Kind = [Microsoft.Win32.RegistryValueKind]::DWord; Value = $value }
|
||||||
|
}
|
||||||
|
'String' {
|
||||||
|
return @{ Name = $valueName; Kind = [Microsoft.Win32.RegistryValueKind]::String; Value = [string]$Operation.ValueData }
|
||||||
|
}
|
||||||
|
'Binary' {
|
||||||
|
return @{ Name = $valueName; Kind = [Microsoft.Win32.RegistryValueKind]::Binary; Value = [byte[]]$Operation.ValueData }
|
||||||
|
}
|
||||||
|
default {
|
||||||
|
throw "Unsupported value type '$valueType' while applying reg operation for '$operationKeyPath'"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)]
|
||||||
|
[string]$RegistryPath,
|
||||||
|
[switch]$CreateIfMissing,
|
||||||
|
[bool]$OpenKey = $true
|
||||||
|
)
|
||||||
|
|
||||||
|
$parts = Split-RegistryPath -path $RegistryPath
|
||||||
|
if (-not $parts) {
|
||||||
|
throw "Unsupported registry path: $RegistryPath"
|
||||||
|
}
|
||||||
|
|
||||||
|
$rootKey = Get-RegistryRootKey -hiveName $parts.Hive
|
||||||
|
if (-not $rootKey) {
|
||||||
|
throw "Unsupported registry hive '$($parts.Hive)' in path '$RegistryPath'"
|
||||||
|
}
|
||||||
|
|
||||||
|
$subKeyPath = $parts.SubKey
|
||||||
|
if ([string]::IsNullOrWhiteSpace($subKeyPath)) {
|
||||||
|
return [PSCustomObject]@{ RootKey = $rootKey; SubKeyPath = $null; Key = $rootKey }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $OpenKey) {
|
||||||
|
return [PSCustomObject]@{ RootKey = $rootKey; SubKeyPath = $subKeyPath; Key = $null }
|
||||||
|
}
|
||||||
|
|
||||||
|
$key = if ($CreateIfMissing) {
|
||||||
|
$rootKey.CreateSubKey($subKeyPath)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$rootKey.OpenSubKey($subKeyPath, $true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return [PSCustomObject]@{ RootKey = $rootKey; SubKeyPath = $subKeyPath; Key = $key }
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-RegistryDeleteValueOperation {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
$Operation,
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
$KeyInfo
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($null -eq $KeyInfo.Key) {
|
||||||
|
$valueName = Get-NormalizedRegistryValueName -ValueName $Operation.ValueName
|
||||||
|
$displayValueName = if ([string]::IsNullOrEmpty($valueName)) { '(Default)' } else { $valueName }
|
||||||
|
Write-Verbose "Unable to find or open key '$($Operation.KeyPath)' and value '$displayValueName'"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$valueName = Get-NormalizedRegistryValueName -ValueName $Operation.ValueName
|
||||||
|
$KeyInfo.Key.DeleteValue($valueName, $false)
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
$KeyInfo.Key.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-RegistrySetValueOperation {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
$Operation,
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
$KeyInfo
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($null -eq $KeyInfo.Key) {
|
||||||
|
throw [System.UnauthorizedAccessException]::new("Unable to open or create registry key '$($Operation.KeyPath)'")
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$setArgs = Convert-RegOperationToValueKind -Operation $Operation
|
||||||
|
$KeyInfo.Key.SetValue($setArgs.Name, $setArgs.Value, $setArgs.Kind)
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
$KeyInfo.Key.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Write-RegistryOperationAccessDeniedWarning {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
$Operation,
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$ExceptionMessage
|
||||||
|
)
|
||||||
|
|
||||||
|
$keyPath = [string]$Operation.KeyPath
|
||||||
|
$operationType = [string]$Operation.OperationType
|
||||||
|
|
||||||
|
if ($operationType -eq 'SetValue' -or $operationType -eq 'DeleteValue') {
|
||||||
|
$valueName = Get-NormalizedRegistryValueName -ValueName $Operation.ValueName
|
||||||
|
$displayValueName = if ([string]::IsNullOrEmpty($valueName)) { '(Default)' } else { $valueName }
|
||||||
|
Write-Warning "Skipping operation '$operationType' on key '$keyPath' value '$displayValueName' due to access restrictions: $ExceptionMessage"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Warning "Skipping operation '$operationType' on key '$keyPath' due to access restrictions: $ExceptionMessage"
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-RegistryOperation {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
$Operation,
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$RegFilePath
|
||||||
|
)
|
||||||
|
|
||||||
|
$operationType = [string]$Operation.OperationType
|
||||||
|
$isSetValueOperation = $operationType -eq 'SetValue'
|
||||||
|
$isDeleteKeyOperation = $operationType -eq 'DeleteKey'
|
||||||
|
|
||||||
|
$keyInfo = Get-RegistryKeyForOperation -RegistryPath $Operation.KeyPath -CreateIfMissing:$isSetValueOperation -OpenKey:(-not $isDeleteKeyOperation)
|
||||||
|
|
||||||
|
switch ($operationType) {
|
||||||
|
'DeleteKey' {
|
||||||
|
if ($null -ne $keyInfo.SubKeyPath) {
|
||||||
|
Remove-RegistrySubKeyTreeIfExists -RootKey $keyInfo.RootKey -SubKeyPath $keyInfo.SubKeyPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'DeleteValue' {
|
||||||
|
Invoke-RegistryDeleteValueOperation -Operation $Operation -KeyInfo $keyInfo
|
||||||
|
}
|
||||||
|
'SetValue' {
|
||||||
|
Invoke-RegistrySetValueOperation -Operation $Operation -KeyInfo $keyInfo
|
||||||
|
}
|
||||||
|
default {
|
||||||
|
throw "Unsupported reg operation type '$($Operation.OperationType)' in '$RegFilePath'"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-RegistryOperationsFromRegFile {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$RegFilePath
|
||||||
|
)
|
||||||
|
|
||||||
|
$accessDeniedCount = 0
|
||||||
|
$operations = @(Get-RegFileOperations -regFilePath $RegFilePath)
|
||||||
|
$totalOperations = $operations.Count
|
||||||
|
|
||||||
|
foreach ($operation in $operations) {
|
||||||
|
try {
|
||||||
|
Invoke-RegistryOperation -Operation $operation -RegFilePath $RegFilePath
|
||||||
|
}
|
||||||
|
catch [System.UnauthorizedAccessException], [System.Security.SecurityException] {
|
||||||
|
$accessDeniedCount++
|
||||||
|
Write-RegistryOperationAccessDeniedWarning -Operation $operation -ExceptionMessage $_.Exception.Message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($totalOperations -gt 0 -and $accessDeniedCount -eq $totalOperations) {
|
||||||
|
throw "Registry fallback import could not apply any operations in '$RegFilePath' because all $accessDeniedCount operation(s) were blocked by access restrictions."
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($accessDeniedCount -gt 0) {
|
||||||
|
Write-Warning "Registry fallback import completed with $accessDeniedCount access-restricted operation(s) skipped in '$RegFilePath'."
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ if (-not $isAdmin) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Define script-level variables & paths
|
# Define script-level variables & paths
|
||||||
$script:Version = "2026.05.10"
|
$script:Version = "2026.05.20"
|
||||||
$configPath = Join-Path $PSScriptRoot 'Config'
|
$configPath = Join-Path $PSScriptRoot 'Config'
|
||||||
$logsPath = Join-Path $PSScriptRoot 'Logs'
|
$logsPath = Join-Path $PSScriptRoot 'Logs'
|
||||||
$schemasPath = Join-Path $PSScriptRoot 'Schemas'
|
$schemasPath = Join-Path $PSScriptRoot 'Schemas'
|
||||||
@@ -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"
|
||||||
@@ -349,6 +350,7 @@ if (-not $script:WingetInstalled -and -not $Silent) {
|
|||||||
. "$PSScriptRoot/Scripts/Helpers/GetUserDirectory.ps1"
|
. "$PSScriptRoot/Scripts/Helpers/GetUserDirectory.ps1"
|
||||||
. "$PSScriptRoot/Scripts/Helpers/GetUserName.ps1"
|
. "$PSScriptRoot/Scripts/Helpers/GetUserName.ps1"
|
||||||
. "$PSScriptRoot/Scripts/Helpers/RegistryPathHelpers.ps1"
|
. "$PSScriptRoot/Scripts/Helpers/RegistryPathHelpers.ps1"
|
||||||
|
. "$PSScriptRoot/Scripts/Helpers/ApplyRegistryRegFile.ps1"
|
||||||
. "$PSScriptRoot/Scripts/Helpers/TestIfUserIsLoggedIn.ps1"
|
. "$PSScriptRoot/Scripts/Helpers/TestIfUserIsLoggedIn.ps1"
|
||||||
|
|
||||||
# Threading functions
|
# Threading functions
|
||||||
@@ -372,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")) {
|
||||||
@@ -401,7 +404,7 @@ else {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($script:Params.ContainsKey("Sysprep")) {
|
if ($script:Params.ContainsKey("Sysprep")) {
|
||||||
$defaultUserPath = GetUserDirectory -userName "Default"
|
GetUserDirectory -userName "Default" | Out-Null
|
||||||
|
|
||||||
# Exit script if run in Sysprep mode on Windows 10
|
# Exit script if run in Sysprep mode on Windows 10
|
||||||
if ($WinVersion -lt 22000) {
|
if ($WinVersion -lt 22000) {
|
||||||
@@ -412,10 +415,10 @@ if ($script:Params.ContainsKey("Sysprep")) {
|
|||||||
|
|
||||||
# Ensure that target user exists, if User or AppRemovalTarget parameter was provided
|
# Ensure that target user exists, if User or AppRemovalTarget parameter was provided
|
||||||
if ($script:Params.ContainsKey("User")) {
|
if ($script:Params.ContainsKey("User")) {
|
||||||
$userPath = GetUserDirectory -userName $script:Params.Item("User")
|
GetUserDirectory -userName $script:Params.Item("User") | Out-Null
|
||||||
}
|
}
|
||||||
if ($script:Params.ContainsKey("AppRemovalTarget")) {
|
if ($script:Params.ContainsKey("AppRemovalTarget")) {
|
||||||
$userPath = GetUserDirectory -userName $script:Params.Item("AppRemovalTarget")
|
GetUserDirectory -userName $script:Params.Item("AppRemovalTarget") | Out-Null
|
||||||
}
|
}
|
||||||
|
|
||||||
# Remove LastUsedSettings.json file if it exists and is empty
|
# Remove LastUsedSettings.json file if it exists and is empty
|
||||||
|
|||||||
Reference in New Issue
Block a user