32 Commits

Author SHA1 Message Date
Jeffrey
85ae8487d6 Refactor ImportRegistryFile function to simplify error handling and improve fallback logic 2026-05-18 21:50:11 +02:00
Jeffrey
7d209e1d23 Remove unused variable $hiveDatPath from ImportRegistryFile 2026-05-18 21:42:21 +02:00
Jeffrey
e6e355341c Remove unsupported registry value types from Convert-RegOperationToValueKind function 2026-05-18 21:34:02 +02:00
Jeffrey
3b0ea16832 Remove unused variables in ImportRegistryFile 2026-05-18 21:28:48 +02:00
Jeffrey
d60e52bdff Refactor ImportRegistryFile function to improve offline hive handling and error reporting 2026-05-18 21:14:20 +02:00
Jeffrey
b0b4615718 Add verbose logging for missing registry keys in Invoke-RegistryDeleteValueOperation 2026-05-17 23:43:35 +02:00
Jeffrey
9a375fdbc4 Refactor registry operation functions 2026-05-17 23:39:41 +02:00
Jeffrey
c3a313c084 Add safety net for offline hive cleanup during registry import fallback 2026-05-17 23:25:03 +02:00
Jeffrey
dd6c65ddb0 Clean up error message 2026-05-17 23:20:00 +02:00
Jeffrey
05bd5f4c56 Refactor Invoke-RegistryOperationsFromRegFile to optimize operation handling and improve access restriction reporting 2026-05-17 23:16:32 +02:00
Jeffrey
c945789d58 Add support for Hex7 type in Convert-RegOperationToValueKind function 2026-05-17 23:13:08 +02:00
Jeffrey
ff90caa338 Refactor registry import functions to improve error handling and streamline hive management 2026-05-17 21:39:35 +02:00
Jeffrey
3363962d64 Add back support for REG_MULTI_SZ in Convert-RegValueData 2026-05-17 20:13:48 +02:00
Jeffrey
cf78eeabce Refactor registry key handling to improve key opening logic and streamline deletion operations 2026-05-17 20:06:14 +02:00
Jeffrey
54830cc928 Enhance error handling and access restriction reporting in registry operations 2026-05-17 20:05:20 +02:00
Jeffrey
4f17727a3b Enhance registry value handling for Hex and Binary types in Convert-RegOperationToValueKind function 2026-05-17 20:00:40 +02:00
Jeffrey
1776e8dae7 Remove REG_MULTI_SZ (not necessary right now) 2026-05-17 19:56:36 +02:00
Jeffrey
bef463df4d Improve registry operation handling 2026-05-17 18:45:51 +02:00
Jeffrey
c7cf793c9e Remove unblocking logic as it doesnt work 2026-05-17 18:39:19 +02:00
Jeffrey
56997dd023 Improve fallback handling and error reporting 2026-05-17 18:27:29 +02:00
Jeffrey
959f79bc35 Add Registry write fall-back in case applying registry file fails 2026-05-17 17:24:08 +02:00
Jeffrey
51aa288dfd Bump version 2026-05-12 00:01:57 +02:00
Jeffrey
24a6f1bcf8 Fix capture and restore of signed dword/qword registry values
Co-authored-by: Copilot <copilot@github.com>
2026-05-11 19:14:08 +02:00
Jeffrey
8ac664e45f Add restart instructions to registry restore success message 2026-05-10 23:26:22 +02:00
Jeffrey
85aa67b5d2 Bump version 2026-05-10 22:27:07 +02:00
Jeffrey
c8b4563954 Fix registry backup validation for sysprep keys 2026-05-09 21:56:58 +02:00
Jeffrey
22f3144c0f Remove > from applytext 2026-05-08 21:26:51 +02:00
Jeffrey
2c360961e3 Add registry backup & restore (#566)
Starting from this commit, Win11Debloat will automatically create a registry backup every time the script is run. This registry backup can be used to revert any registry changes made by the script.
2026-05-08 21:19:52 +02:00
Jeffrey
11a324365d Improve user validation (#568) 2026-05-06 15:54:03 +02:00
Jeffrey
5daa922148 Make tweak columns responsive 2026-04-29 17:05:53 +02:00
Jeffrey
1826d6d8be Refactor app removal scope handling & styling 2026-04-27 15:37:09 +02:00
Jeffrey
c15309bcf6 Update feature labels to include default indicators 2026-04-27 15:30:09 +02:00
41 changed files with 4058 additions and 839 deletions

3
.gitignore vendored
View File

@@ -3,4 +3,5 @@ SavedSettings
LastUsedSettings.json LastUsedSettings.json
CustomAppsList CustomAppsList
Logs/* Logs/*
Win11Debloat.log Win11Debloat.log
Backups/*

File diff suppressed because it is too large Load Diff

View File

@@ -11,18 +11,7 @@
Topmost="False" Topmost="False"
ShowInTaskbar="False"> ShowInTaskbar="False">
<Border BorderBrush="{DynamicResource BorderColor}" <Border Style="{DynamicResource ModalCardBorderStyle}">
BorderThickness="1"
CornerRadius="8"
Background="{DynamicResource CardBgColor}"
Margin="25">
<Border.Effect>
<DropShadowEffect Color="Black"
Opacity="0.15"
BlurRadius="20"
ShadowDepth="0"
Direction="0"/>
</Border.Effect>
<Grid Margin="0"> <Grid Margin="0">
<Grid.RowDefinitions> <Grid.RowDefinitions>
@@ -32,13 +21,9 @@
</Grid.RowDefinitions> </Grid.RowDefinitions>
<!-- Title Bar --> <!-- Title Bar -->
<Grid Grid.Row="0" x:Name="TitleBar" Height="48" Background="Transparent"> <Grid Grid.Row="0" x:Name="TitleBar" Style="{DynamicResource ModalTitleBarStyle}">
<TextBlock Text="About Win11Debloat" <TextBlock Text="About Win11Debloat"
Foreground="{DynamicResource FgColor}" Style="{DynamicResource ModalTitleTextStyle}"/>
FontSize="18"
FontWeight="SemiBold"
VerticalAlignment="Center"
Margin="20,0,0,0"/>
</Grid> </Grid>
<!-- Message Content --> <!-- Message Content -->
@@ -50,7 +35,7 @@
</Grid.RowDefinitions> </Grid.RowDefinitions>
<!-- Message Text --> <!-- Message Text -->
<Grid Grid.Row="0" Margin="24,12,24,20"> <Grid Grid.Row="0" Margin="20,4,20,20">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/> <ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/>
@@ -62,43 +47,31 @@
</Grid.RowDefinitions> </Grid.RowDefinitions>
<!-- Version --> <!-- Version -->
<TextBlock Grid.Row="0" Grid.Column="0" <TextBlock Grid.Row="0" Grid.Column="0"
Text="Version:" Text="Version:"
FontSize="14" Style="{DynamicResource ModalInfoLabelTextStyle}"/>
Foreground="{DynamicResource FgColor}"
FontWeight="SemiBold"
Margin="0,0,16,8"/>
<TextBlock x:Name="VersionText" <TextBlock x:Name="VersionText"
Grid.Row="0" Grid.Column="1" Grid.Row="0" Grid.Column="1"
Text="0.0.0" Text="0.0.0"
FontSize="14" Style="{DynamicResource ModalInfoValueTextStyle}"/>
Foreground="{DynamicResource FgColor}"
Margin="0,0,0,8"/>
<!-- Author --> <!-- Author -->
<TextBlock Grid.Row="1" Grid.Column="0" <TextBlock Grid.Row="1" Grid.Column="0"
Text="Author:" Text="Author:"
FontSize="14" Style="{DynamicResource ModalInfoLabelTextStyle}"/>
Foreground="{DynamicResource FgColor}"
FontWeight="SemiBold"
Margin="0,0,16,8"/>
<TextBlock Grid.Row="1" Grid.Column="1" <TextBlock Grid.Row="1" Grid.Column="1"
Text="Raphire" Text="Raphire"
FontSize="14" Style="{DynamicResource ModalInfoValueTextStyle}"/>
Foreground="{DynamicResource FgColor}"
Margin="0,0,0,8"/>
<!-- Project Link --> <!-- Project Link -->
<TextBlock Grid.Row="2" Grid.Column="0" <TextBlock Grid.Row="2" Grid.Column="0"
Text="Project:" Text="Project:"
FontSize="14" Style="{DynamicResource ModalInfoLabelTextStyle}"
Foreground="{DynamicResource FgColor}"
FontWeight="SemiBold"
Margin="0,0,16,0"/> Margin="0,0,16,0"/>
<TextBlock x:Name="ProjectLink" <TextBlock x:Name="ProjectLink"
Grid.Row="2" Grid.Column="1" Grid.Row="2" Grid.Column="1"
Text="https://github.com/Raphire/Win11Debloat" Text="https://github.com/Raphire/Win11Debloat"
FontSize="14" FontSize="13"
Style="{DynamicResource HyperlinkStyle}" Style="{DynamicResource HyperlinkStyle}"
Margin="0,0,0,0"/> Margin="0,0,0,0"/>
</Grid> </Grid>
@@ -107,10 +80,10 @@
<Border Grid.Row="1" <Border Grid.Row="1"
Height="1" Height="1"
Background="{DynamicResource BorderColor}" Background="{DynamicResource BorderColor}"
Margin="10,0"/> Margin="20,0"/>
<!-- Content --> <!-- Content -->
<StackPanel Grid.Row="2" Margin="24,20"> <StackPanel Grid.Row="2" Margin="20,18,20,20">
<!-- Donation Message --> <!-- Donation Message -->
<TextBlock Text="Win11Debloat is a passion project that I maintain in my free time. If you've found this tool useful, please consider making a small donation to support its development. I really appreciate it!" <TextBlock Text="Win11Debloat is a passion project that I maintain in my free time. If you've found this tool useful, please consider making a small donation to support its development. I really appreciate it!"
FontSize="14" FontSize="14"
@@ -150,19 +123,11 @@
</Grid> </Grid>
<!-- Button Panel --> <!-- Button Panel -->
<Border Grid.Row="2" <Border Grid.Row="2" Style="{DynamicResource ModalFooterBorderStyle}">
Background="{DynamicResource BgColor}" <StackPanel x:Name="ButtonPanel" Style="{DynamicResource ModalFooterButtonsRightStyle}">
BorderBrush="{DynamicResource BorderColor}"
BorderThickness="0,1,0,0"
Padding="16,12"
CornerRadius="0,0,8,8">
<StackPanel x:Name="ButtonPanel"
Orientation="Horizontal"
HorizontalAlignment="Right">
<Button x:Name="CloseButton" <Button x:Name="CloseButton"
Content="Close" Content="Close"
Height="32" MinWidth="80" Margin="4,0" Style="{DynamicResource ModalSecondaryActionButtonStyle}"/>
Style="{DynamicResource SecondaryButtonStyle}"/>
</StackPanel> </StackPanel>
</Border> </Border>
</Grid> </Grid>

View File

@@ -11,18 +11,7 @@
Topmost="False" Topmost="False"
ShowInTaskbar="False"> ShowInTaskbar="False">
<Border BorderBrush="{DynamicResource BorderColor}" <Border Style="{DynamicResource ModalCardBorderStyle}">
BorderThickness="1"
CornerRadius="8"
Background="{DynamicResource CardBgColor}"
Margin="25">
<Border.Effect>
<DropShadowEffect Color="Black"
Opacity="0.15"
BlurRadius="20"
ShadowDepth="0"
Direction="0"/>
</Border.Effect>
<Grid> <Grid>
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
@@ -100,11 +89,7 @@
MaxWidth="430"/> MaxWidth="430"/>
</StackPanel> </StackPanel>
<Border Background="{DynamicResource BgColor}" <Border Style="{DynamicResource ModalFooterBorderStyle}">
BorderBrush="{DynamicResource BorderColor}"
BorderThickness="0,1,0,0"
Padding="16,12"
CornerRadius="0,0,8,8">
<StackPanel> <StackPanel>
<!-- Progress bar --> <!-- Progress bar -->
@@ -172,29 +157,22 @@
</Border> </Border>
<!-- Button Panel --> <!-- Button Panel -->
<Border Background="{DynamicResource BgColor}" <Border Style="{DynamicResource ModalFooterBorderStyle}">
BorderBrush="{DynamicResource BorderColor}" <UniformGrid x:Name="ButtonPanel" Rows="1">
BorderThickness="0,1,0,0" <Button x:Name="ApplyKofiBtn"
Padding="16,12" Style="{DynamicResource ModalSecondaryStretchedButtonStyle}"
CornerRadius="0,0,8,8">
<StackPanel x:Name="ButtonPanel"
Orientation="Horizontal"
HorizontalAlignment="Center">
<Button x:Name="ApplyKofiBtn" Width="210" Height="32"
Style="{DynamicResource SecondaryButtonStyle}"
Margin="0,0,12,0"
AutomationProperties.Name="Support the creator"> AutomationProperties.Name="Support the creator">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center"> <StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="&#xEB52;" FontFamily="Segoe Fluent Icons" FontSize="14" VerticalAlignment="Center" Margin="0,0,8,-1"/> <TextBlock Text="&#xEB52;" FontFamily="Segoe Fluent Icons" FontSize="14" VerticalAlignment="Center" Margin="0,0,8,-1"/>
<TextBlock Text="Support the creator" VerticalAlignment="Center" FontSize="14" Margin="0,0,0,1"/> <TextBlock Text="Support the creator" VerticalAlignment="Center" FontSize="14" Margin="0,0,0,1"/>
</StackPanel> </StackPanel>
</Button> </Button>
<Button x:Name="ApplyCloseBtn" Width="100" Height="32" <Button x:Name="ApplyCloseBtn"
Style="{DynamicResource PrimaryButtonStyle}" Style="{DynamicResource ModalPrimaryStretchedButtonStyle}"
AutomationProperties.Name="Close"> AutomationProperties.Name="Close">
<TextBlock Text="Close" VerticalAlignment="Center" FontSize="14" Margin="0,0,0,1"/> <TextBlock Text="Close" VerticalAlignment="Center" FontSize="14" Margin="0,0,0,1"/>
</Button> </Button>
</StackPanel> </UniformGrid>
</Border> </Border>
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>

View File

@@ -12,18 +12,7 @@
Topmost="False" Topmost="False"
ShowInTaskbar="False"> ShowInTaskbar="False">
<Border BorderBrush="{DynamicResource BorderColor}" <Border Style="{DynamicResource ModalCardBorderStyle}">
BorderThickness="1"
CornerRadius="8"
Background="{DynamicResource CardBgColor}"
Margin="25">
<Border.Effect>
<DropShadowEffect Color="Black"
Opacity="0.15"
BlurRadius="20"
ShadowDepth="0"
Direction="0"/>
</Border.Effect>
<Grid> <Grid>
<Grid.RowDefinitions> <Grid.RowDefinitions>
@@ -33,18 +22,14 @@
</Grid.RowDefinitions> </Grid.RowDefinitions>
<!-- Title Bar --> <!-- Title Bar -->
<Grid Grid.Row="0" x:Name="TitleBar" Height="40" Background="Transparent"> <Grid Grid.Row="0" x:Name="TitleBar" Style="{DynamicResource ModalTitleBarStyle}">
<TextBlock x:Name="TitleText" <TextBlock x:Name="TitleText"
Foreground="{DynamicResource FgColor}" Style="{DynamicResource ModalTitleTextStyle}"/>
FontSize="16"
FontWeight="SemiBold"
VerticalAlignment="Center"
Margin="16,0,0,0"/>
</Grid> </Grid>
<!-- Content --> <!-- Content -->
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto" Padding="0,0,8,0"> <ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto" Padding="0,0,8,0">
<StackPanel x:Name="ContentPanel" Margin="20,12,20,9"> <StackPanel x:Name="ContentPanel" Margin="20,4,20,8">
<TextBlock x:Name="PromptText" <TextBlock x:Name="PromptText"
TextWrapping="Wrap" TextWrapping="Wrap"
FontSize="14" FontSize="14"
@@ -57,22 +42,15 @@
</ScrollViewer> </ScrollViewer>
<!-- Button Footer --> <!-- Button Footer -->
<Border Grid.Row="2" <Border Grid.Row="2" Style="{DynamicResource ModalFooterBorderStyle}">
Background="{DynamicResource BgColor}" <UniformGrid Rows="1">
BorderBrush="{DynamicResource BorderColor}"
BorderThickness="0,1,0,0"
Padding="16,12"
CornerRadius="0,0,8,8">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<Button x:Name="OkButton" <Button x:Name="OkButton"
Content="OK" Content="OK"
Height="32" MinWidth="80" Margin="4,0" Style="{DynamicResource ModalPrimaryStretchedButtonStyle}"/>
Style="{DynamicResource PrimaryButtonStyle}"/>
<Button x:Name="CancelButton" <Button x:Name="CancelButton"
Content="Cancel" Content="Cancel"
Height="32" MinWidth="80" Margin="4,0" Style="{DynamicResource ModalSecondaryStretchedButtonStyle}"/>
Style="{DynamicResource SecondaryButtonStyle}"/> </UniformGrid>
</StackPanel>
</Border> </Border>
</Grid> </Grid>
</Border> </Border>

View File

@@ -2,7 +2,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:shell="clr-namespace:System.Windows.Shell;assembly=PresentationFramework" xmlns:shell="clr-namespace:System.Windows.Shell;assembly=PresentationFramework"
Title="Win11Debloat" Title="Win11Debloat"
MinWidth="1130" MinHeight="600" MinWidth="860" MinHeight="600"
ResizeMode="CanResize" ResizeMode="CanResize"
SnapsToDevicePixels="True" SnapsToDevicePixels="True"
WindowStartupLocation="CenterScreen" WindowStartupLocation="CenterScreen"
@@ -28,177 +28,6 @@
</Trigger> </Trigger>
</Style.Triggers> </Style.Triggers>
</Style> </Style>
<!-- ComboBox Style -->
<Style TargetType="ComboBox">
<Setter Property="Background" Value="{DynamicResource ComboBgColor}"/>
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
<Setter Property="BorderBrush" Value="{DynamicResource BorderColor}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Margin" Value="0,4,0,12"/>
<Setter Property="MinHeight" Value="33"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ComboBox">
<Grid>
<!-- Left accent line -->
<Border x:Name="ClosedAccentLine" Width="3" Height="18" HorizontalAlignment="Left" VerticalAlignment="Stretch" Background="{DynamicResource ButtonBg}" CornerRadius="1.5" Panel.ZIndex="2"/>
<ToggleButton x:Name="ToggleButton" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Focusable="False" IsChecked="{Binding Path=IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" ClickMode="Press">
<ToggleButton.Style>
<Style TargetType="ToggleButton">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ToggleButton">
<Border x:Name="Border" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="4">
<TextBlock x:Name="Arrow"
Text="&#xE70D;"
FontFamily="Segoe Fluent Icons"
FontSize="10"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Margin="0,0,8,0"
Foreground="{DynamicResource FgColor}"
RenderTransformOrigin="0.5,0.5">
<TextBlock.RenderTransform>
<RotateTransform x:Name="ArrowRotation" Angle="0"/>
</TextBlock.RenderTransform>
</TextBlock>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Border" Property="Background" Value="{DynamicResource ComboHoverColor}"/>
</Trigger>
<Trigger Property="IsChecked" Value="True">
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetName="Arrow" Storyboard.TargetProperty="(UIElement.RenderTransform).(RotateTransform.Angle)" To="180" Duration="0:0:0.2">
<DoubleAnimation.EasingFunction>
<CubicEase EasingMode="EaseOut"/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetName="Arrow" Storyboard.TargetProperty="(UIElement.RenderTransform).(RotateTransform.Angle)" To="0" Duration="0:0:0.2">
<DoubleAnimation.EasingFunction>
<CubicEase EasingMode="EaseOut"/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</Trigger.ExitActions>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ToggleButton.Style>
</ToggleButton>
<ContentPresenter x:Name="ContentPresenter"
IsHitTestVisible="False"
Margin="10,0,20,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Content="{TemplateBinding SelectionBoxItem}"
ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}"
ContentTemplateSelector="{TemplateBinding ItemTemplateSelector}"/>
<Popup x:Name="Popup"
Placement="Bottom"
IsOpen="{TemplateBinding IsDropDownOpen}"
AllowsTransparency="True"
Focusable="False"
PopupAnimation="Fade"
StaysOpen="False"
PlacementTarget="{Binding ElementName=ToggleButton}"
VerticalOffset="1"
HorizontalOffset="0">
<Grid x:Name="DropDown" MinWidth="{TemplateBinding ActualWidth}" MaxHeight="{TemplateBinding MaxDropDownHeight}" Margin="12">
<Border x:Name="DropDownBorder"
Background="{DynamicResource ComboItemBgColor}"
BorderBrush="{DynamicResource BorderColor}"
BorderThickness="1"
CornerRadius="4"
Padding="5,4,5,1">
<Border.Effect>
<DropShadowEffect BlurRadius="12" Opacity="0.25" ShadowDepth="4"/>
</Border.Effect>
<ScrollViewer Margin="0,2,0,0"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled">
<ItemsPresenter Margin="0,0,0,1"/>
</ScrollViewer>
</Border>
</Grid>
</Popup>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="SelectedIndex" Value="0">
<Setter TargetName="ClosedAccentLine" Property="Visibility" Value="Collapsed"/>
</Trigger>
<Trigger Property="SelectedIndex" Value="-1">
<Setter TargetName="ClosedAccentLine" Property="Visibility" Value="Collapsed"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="ClosedAccentLine" Property="Visibility" Value="Collapsed"/>
<Setter Property="Background" Value="{DynamicResource ButtonDisabled}"/>
<Setter Property="BorderBrush" Value="{DynamicResource BorderColor}"/>
<Setter Property="Foreground" Value="{DynamicResource ButtonTextDisabled}"/>
<Setter Property="Opacity" Value="0.6"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- ComboBoxItem Style -->
<Style TargetType="ComboBoxItem">
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Padding" Value="10,8"/>
<Setter Property="Margin" Value="0,0,0,4"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ComboBoxItem">
<Grid>
<Border x:Name="ItemBorder"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Padding="{TemplateBinding Padding}"
CornerRadius="4">
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
</Border>
<!-- Left accent line -->
<Border x:Name="AccentLine"
Width="3"
Height="15"
HorizontalAlignment="Left"
VerticalAlignment="Stretch"
Background="{DynamicResource ButtonBg}"
CornerRadius="1.5"
Margin="0"
Visibility="Collapsed"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="AccentLine" Property="Visibility" Value="Visible"/>
<Setter TargetName="ItemBorder" Property="Background" Value="{DynamicResource ComboItemSelectedColor}"/>
</Trigger>
<Trigger Property="IsHighlighted" Value="True">
<Setter TargetName="ItemBorder" Property="Background" Value="{DynamicResource ComboItemHoverColor}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- TextBlock Label Style --> <!-- TextBlock Label Style -->
<Style x:Key="LabelStyle" TargetType="TextBlock"> <Style x:Key="LabelStyle" TargetType="TextBlock">
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/> <Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
@@ -582,6 +411,11 @@
<TextBlock Text="&#xe74e;" FontFamily="Segoe Fluent Icons" FontSize="16" Foreground="{DynamicResource FgColor}"/> <TextBlock Text="&#xe74e;" FontFamily="Segoe Fluent Icons" FontSize="16" Foreground="{DynamicResource FgColor}"/>
</MenuItem.Icon> </MenuItem.Icon>
</MenuItem> </MenuItem>
<MenuItem x:Name="RestoreBackupBtn" Header="Restore backup" AutomationProperties.Name="Restore registry backup">
<MenuItem.Icon>
<TextBlock Text="&#xe81c;" FontFamily="Segoe Fluent Icons" FontSize="16" Foreground="{DynamicResource FgColor}"/>
</MenuItem.Icon>
</MenuItem>
<Separator /> <Separator />
<MenuItem x:Name="MenuDocumentation" Header="Documentation" AutomationProperties.Name="Documentation"> <MenuItem x:Name="MenuDocumentation" Header="Documentation" AutomationProperties.Name="Documentation">
<MenuItem.Icon> <MenuItem.Icon>

View File

@@ -44,34 +44,34 @@
</Grid> </Grid>
<!-- Message Content --> <!-- Message Content -->
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto" MaxHeight="500" Padding="0" Margin="20,12,1,20"> <Grid Grid.Row="1" Margin="20,12,1,20">
<Grid Margin="0,0,20,0"> <Grid.ColumnDefinitions>
<Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/> <ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/> </Grid.ColumnDefinitions>
</Grid.ColumnDefinitions>
<!-- Icon --> <!-- Icon -->
<TextBlock x:Name="IconText" <TextBlock x:Name="IconText"
Grid.Column="0" Grid.Column="0"
FontFamily="Segoe Fluent Icons" FontFamily="Segoe Fluent Icons"
FontSize="24" FontSize="24"
Foreground="{DynamicResource FgColor}" Foreground="{DynamicResource FgColor}"
VerticalAlignment="Center" VerticalAlignment="Center"
Margin="4,2,14,0" Margin="4,0,14,0"
Visibility="Collapsed"/> Visibility="Collapsed"/>
<!-- Message Text --> <!-- Message Text -->
<ScrollViewer Grid.Column="1" VerticalScrollBarVisibility="Auto" MaxHeight="300" Padding="0">
<TextBlock x:Name="MessageText" <TextBlock x:Name="MessageText"
Grid.Column="1"
Text="Message content goes here" Text="Message content goes here"
TextWrapping="Wrap" TextWrapping="Wrap"
FontSize="14" FontSize="14"
LineHeight="20" LineHeight="20"
Foreground="{DynamicResource FgColor}" Foreground="{DynamicResource FgColor}"
VerticalAlignment="Center"/> VerticalAlignment="Center"
</Grid> Margin="0,0,20,0"/>
</ScrollViewer> </ScrollViewer>
</Grid>
<!-- Button Panel --> <!-- Button Panel -->
<Border Grid.Row="2" <Border Grid.Row="2"

View File

@@ -0,0 +1,396 @@
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Restore Backup"
Width="500"
SizeToContent="Height"
MaxHeight="560"
ResizeMode="NoResize"
WindowStartupLocation="CenterOwner"
WindowStyle="None"
AllowsTransparency="True"
Background="Transparent"
Topmost="False"
ShowInTaskbar="False">
<Window.Resources>
<Style x:Key="RestoreOptionTileStyle" TargetType="Button">
<Setter Property="Background" Value="{DynamicResource SecondaryButtonBg}"/>
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderColor}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="12,10"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="8"
Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Left" VerticalAlignment="Center" Margin="0,0,0,1"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Background" Value="{DynamicResource SecondaryButtonDisabled}"/>
<Setter Property="BorderBrush" Value="{DynamicResource BorderColor}"/>
<Setter Property="Foreground" Value="{DynamicResource SecondaryButtonTextDisabled}"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="{DynamicResource SecondaryButtonHover}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderColor}"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Background" Value="{DynamicResource SecondaryButtonPressed}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderColor}"/>
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources>
<Border Style="{DynamicResource ModalCardBorderStyle}">
<Grid Margin="0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid Grid.Row="0" x:Name="TitleBar" Style="{DynamicResource ModalTitleBarStyle}">
<TextBlock x:Name="TitleText"
Text="Restore Backup"
Style="{DynamicResource ModalTitleTextStyle}"/>
</Grid>
<Button x:Name="CloseBtn" Grid.Row="0"
HorizontalAlignment="Right" VerticalAlignment="Top"
Width="36" Height="32"
BorderThickness="0"
Cursor="Hand"
ToolTip="Close"
AutomationProperties.Name="Close">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}" BorderThickness="0" CornerRadius="0,8,0,0">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Button.Template>
<Button.Style>
<Style TargetType="Button">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="{DynamicResource CloseHover}"/>
</Trigger>
</Style.Triggers>
</Style>
</Button.Style>
<TextBlock Text="&#xE8BB;" FontFamily="Segoe Fluent Icons" FontSize="10"/>
</Button>
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto" Margin="0">
<Grid Margin="20,4,20,18">
<TabControl x:Name="RestoreModeTabs"
SelectedIndex="0"
Background="Transparent"
BorderThickness="0"
Padding="0">
<TabControl.ItemContainerStyle>
<Style TargetType="TabItem">
<Setter Property="Visibility" Value="Collapsed"/>
<Setter Property="Height" Value="0"/>
</Style>
</TabControl.ItemContainerStyle>
<TabItem x:Name="SelectTypeTab" Header="SelectType">
<Grid x:Name="SelectTypePanel">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0"
TextWrapping="Wrap"
FontSize="14"
LineHeight="20"
Foreground="{DynamicResource FgColor}"
Text="Choose what changes you want to restore."/>
<Button x:Name="ChooseRegistryBtn"
Grid.Row="1"
Margin="0,14,0,0"
Style="{StaticResource RestoreOptionTileStyle}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0"
Text="&#xEF58;"
FontFamily="Segoe Fluent Icons"
FontSize="24"
VerticalAlignment="Center"
Margin="14,0,14,0"/>
<StackPanel Grid.Column="1"
VerticalAlignment="Center"
Margin="0,0,12,0">
<TextBlock Text="Restore Registry Backup"
FontSize="16"
FontWeight="SemiBold"/>
<TextBlock Text="Restore system registry configuration from a backup"
FontSize="12"
Opacity="0.75"
TextWrapping="Wrap"
Margin="0,2,0,0"/>
</StackPanel>
</Grid>
</Button>
<Button x:Name="ChooseStartMenuBtn"
Grid.Row="2"
Margin="0,10,0,0"
Style="{StaticResource RestoreOptionTileStyle}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0"
Text="&#xE8FC;"
FontFamily="Segoe Fluent Icons"
FontSize="24"
VerticalAlignment="Center"
Margin="14,0,14,0"/>
<StackPanel Grid.Column="1"
VerticalAlignment="Center"
Margin="0,0,12,0">
<TextBlock Text="Restore Start Menu Backup"
FontSize="16"
FontWeight="SemiBold"/>
<TextBlock Text="Restore the Start Menu pinned apps layout from a backup"
FontSize="12"
Opacity="0.75"
TextWrapping="Wrap"
Margin="0,2,0,0"/>
</StackPanel>
</Grid>
</Button>
</Grid>
</TabItem>
<TabItem x:Name="RegistryTab" Header="Registry">
<Grid x:Name="RegistryPanel">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid x:Name="IntroInfoPanel" Grid.Row="0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0"
TextWrapping="Wrap"
FontSize="14"
LineHeight="20"
Foreground="{DynamicResource FgColor}"
Text="This will restore any system registry changes made by Win11Debloat to their previous state. You can review the changes after selecting a backup file. Apps will need to be reinstalled manually."/>
<TextBlock Grid.Row="1"
Margin="0,12,0,2"
TextWrapping="Wrap"
FontSize="13"
Foreground="{DynamicResource FgColor}"
Opacity="0.75"
Text="Warning: Only use backup files generated by Win11Debloat."/>
</Grid>
<Grid x:Name="OverviewPanel" Grid.Row="1" Margin="0" Visibility="Collapsed">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid Grid.Row="0" Margin="0,0,0,16">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Text="File:" Style="{DynamicResource ModalInfoLabelTextStyle}"/>
<TextBlock x:Name="BackupFileText" Grid.Row="0" Grid.Column="1" Text="Not selected" Style="{DynamicResource ModalInfoValueTextStyle}" TextWrapping="Wrap" Margin="0,0,0,8"/>
<TextBlock Grid.Row="1" Grid.Column="0" Text="Created:" Style="{DynamicResource ModalInfoLabelTextStyle}"/>
<TextBlock x:Name="BackupCreatedText" Grid.Row="1" Grid.Column="1" Text="N/A" Style="{DynamicResource ModalInfoValueTextStyle}"/>
<TextBlock Grid.Row="2" Grid.Column="0" Text="Target:" Style="{DynamicResource ModalInfoLabelTextStyle}" Margin="0,0,16,0"/>
<TextBlock x:Name="BackupTargetText" Grid.Row="2" Grid.Column="1" Text="N/A" Style="{DynamicResource ModalInfoValueTextStyle}" Margin="0"/>
</Grid>
<Border Grid.Row="1" Height="1" Background="{DynamicResource BorderColor}" Margin="0,0,0,12"/>
<Grid x:Name="OverviewFeaturesSection" Grid.Row="2">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0"
Text="The following changes will be reverted:"
FontSize="13"
Foreground="{DynamicResource FgColor}"
FontWeight="SemiBold"
Margin="0,0,0,8"/>
<ItemsControl x:Name="FeaturesItemsControl" 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>
<TextBlock x:Name="OverviewSummaryText"
Grid.Row="2"
FontSize="13"
Foreground="{DynamicResource FgColor}"
TextWrapping="Wrap"
Visibility="Collapsed"
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"/>
<Grid x:Name="NonRevertiblePanel" Grid.Row="4" Visibility="Collapsed">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0"
Text="The following changes won't be reverted:"
FontSize="13"
Foreground="{DynamicResource FgColor}"
FontWeight="SemiBold"
Margin="0,0,0,8"/>
<ItemsControl x:Name="NonRevertibleFeaturesItemsControl" 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}" Opacity="0.85" TextWrapping="Wrap" Margin="0,0,0,6"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBlock x:Name="NonRevertibleWikiLink"
Grid.Row="2"
Margin="0,8,0,0"
Text="Visit the wiki for more information"
FontSize="13"
FontWeight="SemiBold"
Style="{DynamicResource HyperlinkStyle}"
TextWrapping="Wrap"/>
</Grid>
</Grid>
</Grid>
</TabItem>
<TabItem x:Name="StartMenuTab" Header="StartMenu">
<Grid x:Name="StartMenuPanel">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid x:Name="StartMenuIntroPanel" Grid.Row="0" Visibility="Visible">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0"
TextWrapping="Wrap"
FontSize="14"
LineHeight="20"
Margin="0,0,0,12"
Foreground="{DynamicResource FgColor}"
Text="This will restore the Start Menu pinned apps layout for the selected user(s) using a backup. Win11Debloat can automatically find the backup created by the script."/>
<ComboBox x:Name="StartMenuScopeCombo"
Grid.Row="1"
SelectedIndex="0"
Margin="0,0,0,2"
MinWidth="408"
HorizontalAlignment="Left">
<ComboBoxItem Content="Current user" Tag="CurrentUser"/>
<ComboBoxItem Content="All users" Tag="AllUsers"/>
</ComboBox>
<CheckBox x:Name="StartMenuAutoBackupCheck"
Grid.Row="2"
Content="Automatically find Start Menu backup"
IsChecked="True"
FontSize="12"
Margin="0,10,0,0"
Foreground="{DynamicResource FgColor}"
Opacity="0.85"/>
</Grid>
</Grid>
</TabItem>
</TabControl>
</Grid>
</ScrollViewer>
<Border Grid.Row="3" Style="{DynamicResource ModalFooterBorderStyle}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Button x:Name="PrimaryActionBtn"
Grid.Column="0"
Content="Next"
Visibility="Collapsed"
Style="{DynamicResource ModalPrimaryStretchedButtonStyle}"/>
<Button x:Name="BackBtn"
Grid.Column="1"
Content="Back"
Visibility="Collapsed"
Style="{DynamicResource ModalSecondaryStretchedButtonStyle}"/>
</Grid>
</Border>
</Grid>
</Border>
</Window>

View File

@@ -18,7 +18,7 @@
BorderThickness="{TemplateBinding BorderThickness}" BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="4" CornerRadius="4"
Padding="{TemplateBinding Padding}"> Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/> <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" Margin="0,0,0,1"/>
</Border> </Border>
</ControlTemplate> </ControlTemplate>
</Setter.Value> </Setter.Value>
@@ -125,6 +125,255 @@
<Setter Property="TextAlignment" Value="Center"/> <Setter Property="TextAlignment" Value="Center"/>
</Style> </Style>
<!-- Shared modal window shell styles -->
<Style x:Key="ModalCardBorderStyle" TargetType="Border">
<Setter Property="BorderBrush" Value="{DynamicResource BorderColor}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="CornerRadius" Value="8"/>
<Setter Property="Background" Value="{DynamicResource CardBgColor}"/>
<Setter Property="Margin" Value="25"/>
<Setter Property="Effect">
<Setter.Value>
<DropShadowEffect Color="Black"
Opacity="0.15"
BlurRadius="20"
ShadowDepth="0"
Direction="0"/>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="ModalTitleBarStyle" TargetType="Grid">
<Setter Property="Height" Value="48"/>
<Setter Property="Background" Value="Transparent"/>
</Style>
<Style x:Key="ModalTitleTextStyle" TargetType="TextBlock">
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
<Setter Property="FontSize" Value="16"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="Margin" Value="20,0,0,0"/>
</Style>
<Style x:Key="ModalFooterBorderStyle" TargetType="Border">
<Setter Property="Background" Value="{DynamicResource BgColor}"/>
<Setter Property="BorderBrush" Value="{DynamicResource BorderColor}"/>
<Setter Property="BorderThickness" Value="0,1,0,0"/>
<Setter Property="Padding" Value="16,12"/>
<Setter Property="CornerRadius" Value="0,0,8,8"/>
</Style>
<Style x:Key="ModalFooterButtonsRightStyle" TargetType="StackPanel">
<Setter Property="Orientation" Value="Horizontal"/>
<Setter Property="HorizontalAlignment" Value="Right"/>
</Style>
<Style x:Key="ModalPrimaryActionButtonStyle" TargetType="Button" BasedOn="{StaticResource PrimaryButtonStyle}">
<Setter Property="Height" Value="32"/>
<Setter Property="MinWidth" Value="80"/>
<Setter Property="Margin" Value="4,0"/>
</Style>
<Style x:Key="ModalSecondaryActionButtonStyle" TargetType="Button" BasedOn="{StaticResource SecondaryButtonStyle}">
<Setter Property="Height" Value="32"/>
<Setter Property="MinWidth" Value="80"/>
<Setter Property="Margin" Value="4,0"/>
</Style>
<Style x:Key="ModalPrimaryStretchedButtonStyle" TargetType="Button" BasedOn="{StaticResource PrimaryButtonStyle}">
<Setter Property="Height" Value="32"/>
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="Margin" Value="4,0"/>
</Style>
<Style x:Key="ModalSecondaryStretchedButtonStyle" TargetType="Button" BasedOn="{StaticResource SecondaryButtonStyle}">
<Setter Property="Height" Value="32"/>
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="Margin" Value="4,0"/>
</Style>
<Style x:Key="ModalInfoLabelTextStyle" TargetType="TextBlock">
<Setter Property="FontSize" Value="13"/>
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Margin" Value="0,0,16,8"/>
</Style>
<Style x:Key="ModalInfoValueTextStyle" TargetType="TextBlock">
<Setter Property="FontSize" Value="13"/>
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
<Setter Property="Margin" Value="0,0,0,8"/>
</Style>
<!-- Shared ComboBox style used across windows -->
<Style TargetType="ComboBoxItem">
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Padding" Value="10,8"/>
<Setter Property="Margin" Value="0,0,0,4"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ComboBoxItem">
<Grid>
<Border x:Name="ItemBorder"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Padding="{TemplateBinding Padding}"
CornerRadius="4">
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
</Border>
<Border x:Name="AccentLine"
Width="3"
Height="15"
HorizontalAlignment="Left"
VerticalAlignment="Stretch"
Background="{DynamicResource ButtonBg}"
CornerRadius="1.5"
Margin="0"
Visibility="Collapsed"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="AccentLine" Property="Visibility" Value="Visible"/>
<Setter TargetName="ItemBorder" Property="Background" Value="{DynamicResource ComboItemSelectedColor}"/>
</Trigger>
<Trigger Property="IsHighlighted" Value="True">
<Setter TargetName="ItemBorder" Property="Background" Value="{DynamicResource ComboItemHoverColor}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="ComboBox">
<Setter Property="Background" Value="{DynamicResource ComboBgColor}"/>
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
<Setter Property="BorderBrush" Value="{DynamicResource BorderColor}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Margin" Value="0,4,0,12"/>
<Setter Property="MinHeight" Value="33"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ComboBox">
<Grid>
<Border x:Name="ClosedAccentLine" Width="3" Height="18" HorizontalAlignment="Left" VerticalAlignment="Stretch" Background="{DynamicResource ButtonBg}" CornerRadius="1.5" Panel.ZIndex="2"/>
<ToggleButton x:Name="ToggleButton" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Focusable="False" IsChecked="{Binding Path=IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" ClickMode="Press">
<ToggleButton.Style>
<Style TargetType="ToggleButton">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ToggleButton">
<Border x:Name="Border" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="4">
<TextBlock x:Name="Arrow"
Text="&#xE70D;"
FontFamily="Segoe Fluent Icons"
FontSize="10"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Margin="0,0,8,0"
Foreground="{DynamicResource FgColor}"
RenderTransformOrigin="0.5,0.5">
<TextBlock.RenderTransform>
<RotateTransform Angle="0"/>
</TextBlock.RenderTransform>
</TextBlock>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Border" Property="Background" Value="{DynamicResource ComboHoverColor}"/>
</Trigger>
<Trigger Property="IsChecked" Value="True">
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetName="Arrow" Storyboard.TargetProperty="(UIElement.RenderTransform).(RotateTransform.Angle)" To="180" Duration="0:0:0.2">
<DoubleAnimation.EasingFunction>
<CubicEase EasingMode="EaseOut"/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetName="Arrow" Storyboard.TargetProperty="(UIElement.RenderTransform).(RotateTransform.Angle)" To="0" Duration="0:0:0.2">
<DoubleAnimation.EasingFunction>
<CubicEase EasingMode="EaseOut"/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</Trigger.ExitActions>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ToggleButton.Style>
</ToggleButton>
<ContentPresenter x:Name="ContentPresenter"
IsHitTestVisible="False"
Margin="10,0,20,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Content="{TemplateBinding SelectionBoxItem}"
ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}"
ContentTemplateSelector="{TemplateBinding ItemTemplateSelector}"/>
<Popup x:Name="Popup"
Placement="Bottom"
IsOpen="{TemplateBinding IsDropDownOpen}"
AllowsTransparency="True"
Focusable="False"
PopupAnimation="Fade"
StaysOpen="False"
PlacementTarget="{Binding ElementName=ToggleButton}"
VerticalOffset="1"
HorizontalOffset="0">
<Grid x:Name="DropDown" MinWidth="{TemplateBinding ActualWidth}" MaxHeight="{TemplateBinding MaxDropDownHeight}" Margin="12">
<Border x:Name="DropDownBorder"
Background="{DynamicResource ComboItemBgColor}"
BorderBrush="{DynamicResource BorderColor}"
BorderThickness="1"
CornerRadius="4"
Padding="5,4,5,1">
<Border.Effect>
<DropShadowEffect BlurRadius="12" Opacity="0.25" ShadowDepth="4"/>
</Border.Effect>
<ScrollViewer Margin="0,2,0,0"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled">
<ItemsPresenter Margin="0,0,0,1"/>
</ScrollViewer>
</Border>
</Grid>
</Popup>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="SelectedIndex" Value="0">
<Setter TargetName="ClosedAccentLine" Property="Visibility" Value="Collapsed"/>
</Trigger>
<Trigger Property="SelectedIndex" Value="-1">
<Setter TargetName="ClosedAccentLine" Property="Visibility" Value="Collapsed"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="ClosedAccentLine" Property="Visibility" Value="Collapsed"/>
<Setter Property="Background" Value="{DynamicResource ButtonDisabled}"/>
<Setter Property="BorderBrush" Value="{DynamicResource BorderColor}"/>
<Setter Property="Foreground" Value="{DynamicResource ButtonTextDisabled}"/>
<Setter Property="Opacity" Value="0.6"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- Base CheckBox style used across windows --> <!-- Base CheckBox style used across windows -->
<Style TargetType="CheckBox"> <Style TargetType="CheckBox">
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/> <Setter Property="Foreground" Value="{DynamicResource FgColor}"/>

View File

@@ -46,9 +46,8 @@ function PrintPendingChanges {
} }
default { default {
if ($script:Features -and $script:Features.ContainsKey($parameterName)) { if ($script:Features -and $script:Features.ContainsKey($parameterName)) {
$action = $script:Features[$parameterName].Action
$message = $script:Features[$parameterName].Label $message = $script:Features[$parameterName].Label
Write-Output "- $action $message" Write-Output "- $message"
} }
else { else {
# Fallback: show the parameter name if no feature description is available # Fallback: show the parameter name if no feature description is available

View File

@@ -0,0 +1,22 @@
function Get-FeatureId {
param(
[Parameter(Mandatory)]
$Feature
)
$featureId = [string]$Feature.FeatureId
if ([string]::IsNullOrWhiteSpace($featureId)) {
throw 'Selected feature is missing required FeatureId.'
}
return $featureId
}
function Get-RegistryBackedFeatures {
param(
[Parameter(Mandatory)]
[object[]]$Features
)
return @($Features | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_.RegistryKey) })
}

View File

@@ -0,0 +1,264 @@
function Get-RegistryBackupCapturePlans {
param(
[Parameter(Mandatory)]
[object[]]$SelectedRegistryFeatures,
[switch]$UseSysprepRegFiles
)
$planMap = @{}
foreach ($feature in $SelectedRegistryFeatures) {
$regFilePath = Get-RegistryFilePathForFeature -Feature $feature -UseSysprepRegFiles:$UseSysprepRegFiles
if (-not (Test-Path $regFilePath)) {
throw "Unable to find registry file for backup: $($feature.RegistryKey) ($regFilePath)"
}
foreach ($operation in @(Get-RegFileOperations -regFilePath $regFilePath)) {
if (-not $operation.KeyPath) { continue }
$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)
}
}
}
}
}
return @(
foreach ($entry in $planMap.Values) {
[PSCustomObject]@{
Path = $entry.Path
IncludeSubKeys = [bool]$entry.IncludeSubKeys
CaptureAllValues = [bool]$entry.CaptureAllValues
ValueNames = @($entry.ValueNames)
}
}
)
}
function Get-RegistrySnapshotsForBackup {
param(
[Parameter(Mandatory)]
[object[]]$CapturePlans
)
if ($CapturePlans.Count -eq 0) {
return @()
}
$snapshotScript = {
param($plans)
$snapshots = @()
foreach ($plan in $plans) {
$snapshots += Get-RegistryKeySnapshot -KeyPath $plan.Path -CaptureAllValues:$plan.CaptureAllValues -ValueNames @($plan.ValueNames) -IncludeSubKeys:$plan.IncludeSubKeys
}
return @($snapshots)
}
if ($script:Params.ContainsKey('Sysprep') -or $script:Params.ContainsKey('User')) {
return Invoke-WithLoadedBackupHive -ScriptBlock $snapshotScript -ArgumentObject @($CapturePlans)
}
return & $snapshotScript $CapturePlans
}
function Invoke-WithLoadedBackupHive {
param(
[Parameter(Mandatory)]
[scriptblock]$ScriptBlock,
$ArgumentObject = $null
)
$hiveDatPath = if ($script:Params.ContainsKey('Sysprep')) {
GetUserDirectory -userName 'Default' -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 to load user hive for registry backup at '$hiveDatPath' (exit code: $loadExitCode)"
}
try {
return & $ScriptBlock $ArgumentObject
}
finally {
$global:LASTEXITCODE = 0
reg unload 'HKU\Default' | Out-Null
$unloadExitCode = $LASTEXITCODE
if ($unloadExitCode -ne 0) {
throw "Failed to unload registry hive 'HKU\Default' (exit code: $unloadExitCode)"
}
}
}
function Get-RegistryKeySnapshot {
param(
[Parameter(Mandatory)]
[string]$KeyPath,
[bool]$CaptureAllValues = $false,
[string[]]$ValueNames = @(),
[bool]$IncludeSubKeys = $false
)
$registryParts = Split-RegistryPath -path $KeyPath
if (-not $registryParts) {
throw "Unsupported registry path in backup: $KeyPath"
}
$rootKey = Get-RegistryRootKey -hiveName $registryParts.Hive
if (-not $rootKey) {
throw "Unsupported registry hive in backup: $($registryParts.Hive)"
}
$subKeyPath = $registryParts.SubKey
$key = $rootKey.OpenSubKey($subKeyPath, $false)
if ($null -eq $key) {
return @{
Path = $KeyPath
Exists = $false
Values = @()
SubKeys = @()
}
}
try {
return (Convert-RegistryKeyToSnapshot -RegistryKey $key -FullPath $KeyPath -CaptureAllValues:$CaptureAllValues -ValueNames $ValueNames -IncludeSubKeys:$IncludeSubKeys)
}
finally {
$key.Close()
}
}
function Convert-RegistryKeyToSnapshot {
param(
[Parameter(Mandatory)]
[Microsoft.Win32.RegistryKey]$RegistryKey,
[Parameter(Mandatory)]
[string]$FullPath,
[bool]$CaptureAllValues = $false,
[string[]]$ValueNames = @(),
[bool]$IncludeSubKeys = $false
)
$values = @()
if ($CaptureAllValues) {
foreach ($valueName in @($RegistryKey.GetValueNames())) {
$values += @(Convert-RegistryValueToSnapshot -RegistryKey $RegistryKey -ValueName $valueName)
}
}
else {
foreach ($valueName in @($ValueNames | Sort-Object -Unique)) {
$exists = ($RegistryKey.GetValueNames() -contains $valueName)
if ($exists) {
$values += @(Convert-RegistryValueToSnapshot -RegistryKey $RegistryKey -ValueName $valueName)
}
else {
$values += @{
Name = $valueName
Exists = $false
Kind = $null
Data = $null
}
}
}
}
$subKeys = @()
if ($IncludeSubKeys) {
foreach ($subKeyName in @($RegistryKey.GetSubKeyNames())) {
$childKey = $RegistryKey.OpenSubKey($subKeyName, $false)
if ($null -eq $childKey) { continue }
try {
$childPath = if ([string]::IsNullOrWhiteSpace($FullPath)) { $subKeyName } else { "$FullPath\$subKeyName" }
$subKeys += @(Convert-RegistryKeyToSnapshot -RegistryKey $childKey -FullPath $childPath -CaptureAllValues:$true -IncludeSubKeys:$true)
}
finally {
$childKey.Close()
}
}
}
return @{
Path = $FullPath
Exists = $true
Values = $values
SubKeys = $subKeys
}
}
function Convert-RegistryValueToSnapshot {
param(
[Parameter(Mandatory)]
[Microsoft.Win32.RegistryKey]$RegistryKey,
[Parameter(Mandatory)]
[AllowEmptyString()]
[string]$ValueName
)
$valueKind = $RegistryKey.GetValueKind($ValueName)
$value = $RegistryKey.GetValue($ValueName, $null, [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)
try {
$normalizedValue = switch ($valueKind) {
([Microsoft.Win32.RegistryValueKind]::Binary) { @($value | ForEach-Object { [int]$_ }) }
([Microsoft.Win32.RegistryValueKind]::MultiString) { @($value) }
([Microsoft.Win32.RegistryValueKind]::DWord) { [BitConverter]::ToUInt32([BitConverter]::GetBytes([int32]$value), 0) }
([Microsoft.Win32.RegistryValueKind]::QWord) { [BitConverter]::ToUInt64([BitConverter]::GetBytes([int64]$value), 0) }
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 @{
Name = $ValueName
Exists = $true
Kind = $valueKind.ToString()
Data = $normalizedValue
}
}
function Get-RegistryBackupTargetDescription {
if ($script:Params.ContainsKey('Sysprep')) {
return 'DefaultUserProfile'
}
$resolvedUserName = [string](GetUserName)
if ($script:Params.ContainsKey('User')) {
return "User:$resolvedUserName"
}
return "CurrentUser:$resolvedUserName"
}

View File

@@ -0,0 +1,88 @@
function New-RegistrySettingsBackup {
param(
[string[]]$ActionableKeys
)
$ActionableKeys = @($ActionableKeys)
$selectedFeatures = Get-SelectedFeatures -ActionableKeys $ActionableKeys
if (@($selectedFeatures | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_.RegistryKey) }).Count -eq 0) {
return $null
}
$timestamp = Get-Date
$backupDirectory = $script:RegistryBackupsPath
if (-not (Test-Path $backupDirectory)) {
New-Item -ItemType Directory -Path $backupDirectory -Force | Out-Null
}
$backupFileName = 'Win11Debloat-RegistryBackup-{0}.json' -f $timestamp.ToString('yyyyMMdd_HHmmss')
$backupFilePath = Join-Path $backupDirectory $backupFileName
$backupConfig = Get-RegistryBackupPayload -SelectedFeatures $selectedFeatures -CreatedAt $timestamp
if (-not (SaveToFile -Config $backupConfig -FilePath $backupFilePath -MaxDepth 25)) {
throw "Failed to save registry backup to '$backupFilePath'"
}
Write-Host "Backup successfully created: $backupFilePath"
Write-Host ""
return $backupFilePath
}
function Get-SelectedFeatures {
param(
[string[]]$ActionableKeys
)
$selectedFeatures = New-Object System.Collections.Generic.List[object]
$selectedFeatureIds = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
foreach ($paramKey in $ActionableKeys) {
if (-not $script:Features.ContainsKey($paramKey)) { continue }
$feature = $script:Features[$paramKey]
if (-not $feature) { continue }
$featureId = Get-FeatureId -Feature $feature
if ($selectedFeatureIds.Add($featureId)) {
$selectedFeatures.Add($feature)
}
}
return @($selectedFeatures.ToArray())
}
function Get-RegistryBackupPayload {
param(
[Parameter(Mandatory)]
[object[]]$SelectedFeatures,
[Parameter(Mandatory)]
[datetime]$CreatedAt
)
$selectedFeatureIds = New-Object System.Collections.Generic.List[string]
$seenSelectedFeatureIds = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
foreach ($feature in $SelectedFeatures) {
$featureId = Get-FeatureId -Feature $feature
if ($seenSelectedFeatureIds.Add($featureId)) {
$selectedFeatureIds.Add($featureId)
}
}
$selectedRegistryFeatures = Get-RegistryBackedFeatures -Features $SelectedFeatures
$capturePlans = Get-RegistryBackupCapturePlans -SelectedRegistryFeatures $SelectedRegistryFeatures
$registryKeys = @(Get-RegistrySnapshotsForBackup -CapturePlans $capturePlans)
return @{
Version = '1.0'
BackupType = 'RegistryState'
CreatedAt = $CreatedAt.ToString('o')
CreatedBy = 'Win11Debloat'
Target = (Get-RegistryBackupTargetDescription)
ComputerName = $env:COMPUTERNAME
SelectedFeatures = @($selectedFeatureIds)
RegistryKeys = @($registryKeys)
}
}

View File

@@ -2,7 +2,7 @@
function DisableStoreSearchSuggestionsForAllUsers { function DisableStoreSearchSuggestionsForAllUsers {
# Get path to Store app database for all users # Get path to Store app database for all users
$userPathString = GetUserDirectory -userName "*" -fileName "AppData\Local\Packages" $userPathString = GetUserDirectory -userName "*" -fileName "AppData\Local\Packages"
$usersStoreDbPaths = get-childitem -path $userPathString $usersStoreDbPaths = Get-ChildItem -Path $userPathString -ErrorAction SilentlyContinue
# Go through all users and disable start search suggestions # Go through all users and disable start search suggestions
ForEach ($storeDbPath in $usersStoreDbPaths) { ForEach ($storeDbPath in $usersStoreDbPaths) {

View File

@@ -146,10 +146,37 @@ function ExecuteAllChanges {
if ($paramKey -eq 'CreateRestorePoint') { continue } if ($paramKey -eq 'CreateRestorePoint') { continue }
$actionableKeys += $paramKey $actionableKeys += $paramKey
} }
$hasRegistryBackedFeature = $false
foreach ($paramKey in $actionableKeys) {
if (-not $script:Features.ContainsKey($paramKey)) { continue }
$feature = $script:Features[$paramKey]
if ($feature -and -not [string]::IsNullOrWhiteSpace([string]$feature.RegistryKey)) {
$hasRegistryBackedFeature = $true
break
}
}
$totalSteps = $actionableKeys.Count $totalSteps = $actionableKeys.Count
if ($hasRegistryBackedFeature) { $totalSteps++ }
if ($script:Params.ContainsKey("CreateRestorePoint")) { $totalSteps++ } if ($script:Params.ContainsKey("CreateRestorePoint")) { $totalSteps++ }
$currentStep = 0 $currentStep = 0
if ($hasRegistryBackedFeature) {
$currentStep++
if ($script:ApplyProgressCallback) {
& $script:ApplyProgressCallback $currentStep $totalSteps "Creating registry backup"
}
Write-Host "> Creating registry backup..."
try {
New-RegistrySettingsBackup -ActionableKeys $actionableKeys | 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")) {
@@ -178,12 +205,8 @@ function ExecuteAllChanges {
# Prefer explicit ApplyText when provided # Prefer explicit ApplyText when provided
$stepName = $feature.ApplyText $stepName = $feature.ApplyText
} elseif ($feature.Label) { } elseif ($feature.Label) {
# Fallback: construct a name from Action and Label, or just Label # Fallback: use label from Features.json
if ($feature.Action) { $stepName = $feature.Label
$stepName = "$($feature.Action) $($feature.Label)"
} else {
$stepName = $feature.Label
}
} }
} }

View File

@@ -8,12 +8,13 @@ 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) { $regFileDirectory = if ($usesOfflineHive) {
"$script:RegfilesPath\Sysprep\$path" Join-Path $script:RegfilesPath "Sysprep"
} }
else { else {
"$script:RegfilesPath\$path" $script:RegfilesPath
} }
$regFilePath = Join-Path $regFileDirectory $path
if (-not (Test-Path $regFilePath)) { if (-not (Test-Path $regFilePath)) {
$errorMessage = "Unable to find registry file: $path ($regFilePath)" $errorMessage = "Unable to find registry file: $path ($regFilePath)"
@@ -22,19 +23,28 @@ function ImportRegistryFile {
throw $errorMessage throw $errorMessage
} }
# Reset exit code before running reg.exe for reliable success detection $regResult = $null
$global:LASTEXITCODE = 0 $offlineHiveLoaded = $false
if ($usesOfflineHive) { try {
# Sysprep targets Default user, User targets the specified user if ($usesOfflineHive) {
$hiveDatPath = if ($script:Params.ContainsKey("Sysprep")) { # Sysprep targets Default user, User targets the specified user
GetUserDirectory -userName "Default" -fileName "NTUSER.DAT" $targetUserName = if ($script:Params.ContainsKey("Sysprep")) { "Default" } else { $script:Params.Item("User") }
} else { $hiveDatPath = GetUserDirectory -userName $targetUserName -fileName "NTUSER.DAT"
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 +53,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,52 +69,50 @@ 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 temporary 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
if ($regOutput) { if ($regOutput) {
foreach ($line in $regOutput) { foreach ($line in $regOutput) {
$lineText = if ($line -is [System.Management.Automation.ErrorRecord]) { $line.Exception.Message } else { $line.ToString() } $lineText = if ($line -is [System.Management.Automation.ErrorRecord]) { $line.Exception.Message } else { $line.ToString() }
if ($lineText -and $lineText.Length -gt 0) { if ($lineText -and $lineText.Length -gt 0) {
if ($hasSuccess) { if ($hasSuccess) {
Write-Host $lineText Write-Host $lineText
} }
else { else {
Write-Host $lineText -ForegroundColor Red Write-Host $lineText -ForegroundColor Red
}
} }
} }
} }
}
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 "Fallback import succeeded for '$path'." -ForegroundColor Yellow
}
Write-Host "" Write-Host ""
throw $errorMessage
} }
catch {
Write-Host $_.Exception.Message -ForegroundColor Red
Write-Host ""
throw
}
finally {
if ($offlineHiveLoaded) {
$global:LASTEXITCODE = 0
reg unload "HKU\Default" | Out-Null
$unloadExitCode = $LASTEXITCODE
Write-Host "" if ($unloadExitCode -ne 0) {
throw "Failed to unload registry hive HKU\Default after importing '$path' (exit code: $unloadExitCode)"
}
}
}
} }

View File

@@ -0,0 +1,385 @@
function Get-NormalizedSelectedFeatureIdsFromBackup {
param(
[Parameter(Mandatory)]
$Backup
)
$selectedFeatures = New-Object System.Collections.Generic.List[string]
$selectedFeatureIds = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
$errors = New-Object System.Collections.Generic.List[string]
$hasInvalidSelectedFeatureId = $false
if (-not $Backup.PSObject.Properties['SelectedFeatures']) {
$errors.Add('Missing property: SelectedFeatures')
return [PSCustomObject]@{
SelectedFeatures = $selectedFeatures.ToArray()
Errors = $errors.ToArray()
}
}
foreach ($featureId in @($Backup.SelectedFeatures)) {
if ($featureId -isnot [string] -or [string]::IsNullOrWhiteSpace([string]$featureId)) {
$hasInvalidSelectedFeatureId = $true
continue
}
$normalizedFeatureId = [string]$featureId
if ($selectedFeatureIds.Add($normalizedFeatureId)) {
$selectedFeatures.Add($normalizedFeatureId)
}
}
if ($hasInvalidSelectedFeatureId) {
$errors.Add('SelectedFeatures must contain non-empty string feature IDs.')
}
if ($selectedFeatures.Count -eq 0) {
$errors.Add('SelectedFeatures must contain at least one feature ID.')
}
return [PSCustomObject]@{
SelectedFeatures = $selectedFeatures.ToArray()
Errors = $errors.ToArray()
}
}
function Normalize-RegistryKeySnapshot {
param(
[Parameter(Mandatory)]
$Snapshot
)
if (-not $Snapshot.PSObject.Properties['Path'] -or [string]::IsNullOrWhiteSpace([string]$Snapshot.Path)) {
throw 'Backup validation failed: Registry key snapshot is missing Path.'
}
$exists = $false
if ($Snapshot.PSObject.Properties['Exists']) {
$exists = [bool]$Snapshot.Exists
}
$values = @()
if ($Snapshot.PSObject.Properties['Values']) {
foreach ($valueSnapshot in @($Snapshot.Values)) {
$valueExists = $true
if ($valueSnapshot.PSObject.Properties['Exists']) {
$valueExists = [bool]$valueSnapshot.Exists
}
$values += [PSCustomObject]@{
Name = [string]$valueSnapshot.Name
Exists = $valueExists
Kind = if ($valueSnapshot.PSObject.Properties['Kind']) { [string]$valueSnapshot.Kind } else { $null }
Data = if ($valueSnapshot.PSObject.Properties['Data']) { $valueSnapshot.Data } else { $null }
}
}
}
$subKeys = @()
if ($Snapshot.PSObject.Properties['SubKeys']) {
foreach ($subKeySnapshot in @($Snapshot.SubKeys)) {
$subKeys += @(Normalize-RegistryKeySnapshot -Snapshot $subKeySnapshot)
}
}
return [PSCustomObject]@{
Path = [string]$Snapshot.Path
Exists = $exists
Values = @($values)
SubKeys = @($subKeys)
}
}
function Test-RegistryBackupMatchesSelectedFeatures {
param(
[Parameter(Mandatory)]
[AllowEmptyCollection()]
[string[]]$SelectedFeatureIds,
[Parameter(Mandatory)]
[string]$Target,
[Parameter(Mandatory)]
[AllowEmptyCollection()]
[object[]]$RegistryKeys
)
$errors = New-Object System.Collections.Generic.List[string]
if (-not $script:Features -or $script:Features.Count -eq 0) {
$errors.Add('Unable to validate registry backup allowlist because feature definitions are not loaded.')
return $errors.ToArray()
}
$selectedRegistryFeatures = @(Get-SelectedRegistryFeaturesForBackupValidation -SelectedFeatureIds @($SelectedFeatureIds) -Errors $errors)
$useSysprepRegFiles = ($Target -eq 'DefaultUserProfile') -or ($Target -like 'User:*')
$capturePlans = @()
if ($errors.Count -eq 0 -and $selectedRegistryFeatures.Count -gt 0) {
$capturePlans = @(Get-RegistryBackupCapturePlans -SelectedRegistryFeatures @($selectedRegistryFeatures) -UseSysprepRegFiles:$useSysprepRegFiles)
}
$planMap = New-RegistryBackupAllowListPlanMap -CapturePlans @($capturePlans)
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.')
}
foreach ($rootSnapshot in @($RegistryKeys)) {
Test-RegistrySnapshotAgainstAllowList -Snapshot $rootSnapshot -PlanMap $planMap -Errors $errors
}
return $errors.ToArray()
}
function Get-SelectedRegistryFeaturesForBackupValidation {
param(
[Parameter(Mandatory)]
[AllowEmptyCollection()]
[string[]]$SelectedFeatureIds,
[Parameter(Mandatory)]
[AllowEmptyCollection()]
$Errors
)
if ($null -eq $Errors -or -not ($Errors -is [System.Collections.IList])) {
throw 'Get-SelectedRegistryFeaturesForBackupValidation requires Errors to be a mutable list collection.'
}
$selectedRegistryFeatures = New-Object System.Collections.Generic.List[object]
foreach ($featureId in @($SelectedFeatureIds)) {
if (-not $script:Features.ContainsKey($featureId)) {
$Errors.Add("Selected feature '$featureId' was not found in the current feature catalog.")
continue
}
$feature = $script:Features[$featureId]
if ($feature -and -not [string]::IsNullOrWhiteSpace([string]$feature.RegistryKey)) {
$selectedRegistryFeatures.Add($feature)
}
}
return $selectedRegistryFeatures.ToArray()
}
function New-RegistryBackupAllowListPlanMap {
param(
[Parameter(Mandatory)]
[AllowEmptyCollection()]
[object[]]$CapturePlans
)
$planMap = @{}
foreach ($plan in @($CapturePlans)) {
$normalizedPath = Get-NormalizedRegistryPathKey -Path $plan.Path
if ([string]::IsNullOrWhiteSpace($normalizedPath)) {
continue
}
$planMap[$normalizedPath] = [PSCustomObject]@{
Path = $plan.Path
NormalizedPath = $normalizedPath
IncludeSubKeys = [bool]$plan.IncludeSubKeys
CaptureAllValues = [bool]$plan.CaptureAllValues
ValueNames = ConvertTo-RegistryValueNameSet -ValueNames @($plan.ValueNames)
}
}
return $planMap
}
function ConvertTo-RegistryValueNameSet {
param(
[AllowEmptyCollection()]
[string[]]$ValueNames
)
$valueNameSet = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
foreach ($valueName in @($ValueNames)) {
$null = $valueNameSet.Add([string]$valueName)
}
return $valueNameSet
}
function Test-RegistrySnapshotAgainstAllowList {
param(
[Parameter(Mandatory)]
$Snapshot,
[Parameter(Mandatory)]
[hashtable]$PlanMap,
[Parameter(Mandatory)]
[AllowEmptyCollection()]
[System.Collections.Generic.List[string]]$Errors
)
$snapshotPath = [string]$Snapshot.Path
$normalizedPath = Get-NormalizedRegistryPathKey -Path $snapshotPath
if ([string]::IsNullOrWhiteSpace($normalizedPath)) {
$Errors.Add("Backup contains unsupported registry path '$snapshotPath'.")
return
}
$planMatch = Find-RegistryAllowListPlanMatch -NormalizedPath $normalizedPath -PlanMap $PlanMap
if ($null -eq $planMatch) {
$Errors.Add("Backup contains unexpected registry path '$snapshotPath' that is not allowed by SelectedFeatures.")
return
}
foreach ($valueSnapshot in @($Snapshot.Values)) {
$valueName = Get-NormalizedRegistryValueName -ValueName $valueSnapshot.Name
$valueExists = [bool]$valueSnapshot.Exists
if (-not (Test-RegistryValueAllowedByPlan -PlanMatch $planMatch -ValueName $valueName)) {
$Errors.Add("Backup contains unexpected value '$valueName' under '$snapshotPath'.")
}
$kindName = if ($valueSnapshot.PSObject.Properties['Kind']) { [string]$valueSnapshot.Kind } else { '' }
$valueReference = Get-RegistryValueReferenceForError -SnapshotPath $snapshotPath -ValueName $valueName
if ($valueExists) {
if (-not (Test-RegistryValueKindNameSupported -KindName $kindName)) {
$Errors.Add("Backup contains unsupported registry value kind '$kindName' for '$valueReference'.")
}
}
elseif (-not [string]::IsNullOrWhiteSpace($kindName)) {
$Errors.Add("Backup value '$valueReference' must not define Kind when Exists is false.")
}
}
foreach ($subKeySnapshot in @($Snapshot.SubKeys)) {
Test-RegistrySnapshotAgainstAllowList -Snapshot $subKeySnapshot -PlanMap $PlanMap -Errors $Errors
}
}
function Test-RegistryValueAllowedByPlan {
param(
[Parameter(Mandatory)]
$PlanMatch,
[Parameter(Mandatory)]
[AllowNull()]
[AllowEmptyString()]
[string]$ValueName
)
$ValueName = Get-NormalizedRegistryValueName -ValueName $ValueName
if ($PlanMatch.CaptureAllValues -or $PlanMatch.IsDescendant) {
return $true
}
return $PlanMatch.ValueNames.Contains($ValueName)
}
function Get-RegistryValueReferenceForError {
param(
[Parameter(Mandatory)]
[string]$SnapshotPath,
[Parameter(Mandatory)]
[AllowNull()]
[AllowEmptyString()]
[string]$ValueName
)
$ValueName = Get-NormalizedRegistryValueName -ValueName $ValueName
if ([string]::IsNullOrWhiteSpace($ValueName)) {
return "$SnapshotPath\\(Default)"
}
return "$SnapshotPath\\$ValueName"
}
function Get-NormalizedRegistryValueName {
param(
[AllowNull()]
[AllowEmptyString()]
[object]$ValueName
)
if ($null -eq $ValueName) {
return ''
}
return [string]$ValueName
}
function Find-RegistryAllowListPlanMatch {
param(
[Parameter(Mandatory)]
[string]$NormalizedPath,
[Parameter(Mandatory)]
[hashtable]$PlanMap
)
if ($PlanMap.ContainsKey($NormalizedPath)) {
$plan = $PlanMap[$NormalizedPath]
return [PSCustomObject]@{
IsDescendant = $false
CaptureAllValues = [bool]$plan.CaptureAllValues
ValueNames = $plan.ValueNames
}
}
foreach ($plan in @($PlanMap.Values)) {
if (-not [bool]$plan.IncludeSubKeys) {
continue
}
$subKeyPrefix = "$($plan.NormalizedPath)\\"
if ($NormalizedPath.StartsWith($subKeyPrefix, [System.StringComparison]::OrdinalIgnoreCase)) {
return [PSCustomObject]@{
IsDescendant = $true
CaptureAllValues = $true
ValueNames = $plan.ValueNames
}
}
}
return $null
}
function Get-NormalizedRegistryPathKey {
param(
[Parameter(Mandatory)]
[string]$Path
)
$parts = Split-RegistryPath -path $Path
if (-not $parts) {
return $null
}
$hiveName = [string]$parts.Hive
if ([string]::IsNullOrWhiteSpace($hiveName)) {
return $null
}
$normalizedHive = $hiveName.ToUpperInvariant()
$subKey = [string]$parts.SubKey
if ([string]::IsNullOrWhiteSpace($subKey)) {
return $normalizedHive
}
$normalizedSubKey = ($subKey -replace '/', '\\').Trim('\')
if ([string]::IsNullOrWhiteSpace($normalizedSubKey)) {
return $normalizedHive
}
return "$normalizedHive\\$normalizedSubKey"
}
function Test-RegistryValueKindNameSupported {
param(
[string]$KindName
)
if ([string]::IsNullOrWhiteSpace($KindName)) {
return $false
}
try {
$kind = [System.Enum]::Parse([Microsoft.Win32.RegistryValueKind], $KindName, $true)
return $kind -ne [Microsoft.Win32.RegistryValueKind]::Unknown
}
catch {
return $false
}
}

View File

@@ -2,7 +2,7 @@
# Credit: https://lazyadmin.nl/win-11/customize-windows-11-start-menu-layout/ # Credit: https://lazyadmin.nl/win-11/customize-windows-11-start-menu-layout/
function ReplaceStartMenuForAllUsers { function ReplaceStartMenuForAllUsers {
param ( param (
$startMenuTemplate = "$script:AssetsPath/Start/start2.bin" $startMenuTemplate = "$script:AssetsPath\Start\start2.bin"
) )
Write-Host "> Removing all pinned apps from the start menu for all users..." Write-Host "> Removing all pinned apps from the start menu for all users..."
@@ -16,7 +16,7 @@ function ReplaceStartMenuForAllUsers {
# Get path to start menu file for all users # Get path to start menu file for all users
$userPathString = GetUserDirectory -userName "*" -fileName "AppData\Local\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState" $userPathString = GetUserDirectory -userName "*" -fileName "AppData\Local\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState"
$usersStartMenuPaths = get-childitem -path $userPathString $usersStartMenuPaths = Get-ChildItem -Path $userPathString -ErrorAction SilentlyContinue
# Go through all users and replace the start menu file # Go through all users and replace the start menu file
ForEach ($startMenuPath in $usersStartMenuPaths) { ForEach ($startMenuPath in $usersStartMenuPaths) {
@@ -43,13 +43,13 @@ function ReplaceStartMenuForAllUsers {
# Credit: https://lazyadmin.nl/win-11/customize-windows-11-start-menu-layout/ # Credit: https://lazyadmin.nl/win-11/customize-windows-11-start-menu-layout/
function ReplaceStartMenu { function ReplaceStartMenu {
param ( param (
$startMenuTemplate = "$script:AssetsPath/Start/start2.bin", $startMenuTemplate = "$script:AssetsPath\Start\start2.bin",
$startMenuBinFile = "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin" $startMenuBinFile = "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin"
) )
# Change path to correct user if a user was specified # Change path to correct user if a user was specified
if ($script:Params.ContainsKey("User")) { if ($script:Params.ContainsKey("User")) {
$startMenuBinFile = GetUserDirectory -userName "$(GetUserName)" -fileName "AppData\Local\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin" -exitIfPathNotFound $false $startMenuBinFile = GetStartMenuBinPathForUser -UserName (GetUserName)
} }
# Check if template bin file exists # Check if template bin file exists
@@ -63,7 +63,7 @@ function ReplaceStartMenu {
return return
} }
$userName = [regex]::Match($startMenuBinFile, '(?:Users\\)([^\\]+)(?:\\AppData)').Groups[1].Value $userName = GetStartMenuUserNameFromPath -StartMenuBinFile $startMenuBinFile
$backupBinFile = $startMenuBinFile + ".bak" $backupBinFile = $startMenuBinFile + ".bak"
@@ -80,4 +80,142 @@ function ReplaceStartMenu {
Copy-Item -Path $startMenuTemplate -Destination $startMenuBinFile -Force Copy-Item -Path $startMenuTemplate -Destination $startMenuBinFile -Force
Write-Host "Replaced start menu for user $userName" Write-Host "Replaced start menu for user $userName"
}
function GetStartMenuBinPathForUser {
param(
[string]$UserName
)
if ([string]::IsNullOrWhiteSpace($UserName)) {
return "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin"
}
return (GetUserDirectory -userName $UserName -fileName "AppData\Local\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin" -exitIfPathNotFound $false)
}
function GetStartMenuUserNameFromPath {
param(
[string]$StartMenuBinFile
)
$resolvedUserName = [regex]::Match($StartMenuBinFile, '(?:Users\\)([^\\]+)(?:\\AppData)').Groups[1].Value
if ([string]::IsNullOrWhiteSpace($resolvedUserName)) {
return 'unknown'
}
return $resolvedUserName
}
function RestoreStartMenuFromBackup {
param(
[Parameter(Mandatory)]
[string]$StartMenuBinFile,
[Parameter(Mandatory = $false)]
[string]$BackupFilePath
)
$userName = GetStartMenuUserNameFromPath -StartMenuBinFile $StartMenuBinFile
$backupBinFile = if ([string]::IsNullOrWhiteSpace($BackupFilePath)) {
$StartMenuBinFile + '.bak'
}
else {
$BackupFilePath
}
$currentBinBackup = $StartMenuBinFile + '.restore.bak'
if (-not (Test-Path -LiteralPath $backupBinFile)) {
return [PSCustomObject]@{
UserName = $userName
Result = $false
Message = "No start menu backup file found for user $userName."
}
}
try {
if (Test-Path -LiteralPath $StartMenuBinFile) {
Move-Item -Path $StartMenuBinFile -Destination $currentBinBackup -Force
}
Copy-Item -Path $backupBinFile -Destination $StartMenuBinFile -Force
return [PSCustomObject]@{
UserName = $userName
Result = $true
Message = "Restored start menu for user $userName."
}
}
catch {
return [PSCustomObject]@{
UserName = $userName
Result = $false
Message = "Failed to restore start menu for user $userName. $($_.Exception.Message)"
}
}
}
function RestoreStartMenu {
param(
[Parameter(Mandatory = $false)]
[string]$BackupFilePath
)
$targetUserName = GetUserName
$startMenuBinFile = GetStartMenuBinPathForUser -UserName $targetUserName
Write-Host "Restoring start menu for user $targetUserName from backup..."
return RestoreStartMenuFromBackup -StartMenuBinFile $startMenuBinFile -BackupFilePath $BackupFilePath
}
function RestoreStartMenuForAllUsers {
param(
[Parameter(Mandatory = $false)]
[string]$BackupFilePath
)
$userPathString = GetUserDirectory -userName "*" -fileName "AppData\Local\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState"
$usersStartMenuPaths = Get-ChildItem -Path $userPathString -ErrorAction SilentlyContinue
$results = @()
Write-Host "Restoring start menu for all users from backup..."
foreach ($startMenuPath in $usersStartMenuPaths) {
$startMenuBinFile = Join-Path $startMenuPath.FullName 'start2.bin'
$results += RestoreStartMenuFromBackup -StartMenuBinFile $startMenuBinFile -BackupFilePath $BackupFilePath
}
$defaultStartMenuPath = GetUserDirectory -userName "Default" -fileName "AppData\Local\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState" -exitIfPathNotFound $false
if (Test-Path $defaultStartMenuPath) {
$defaultStartMenuBinFile = Join-Path $defaultStartMenuPath 'start2.bin'
if (Test-Path -LiteralPath $defaultStartMenuBinFile) {
try {
Remove-Item -LiteralPath $defaultStartMenuBinFile -Force
$results += [PSCustomObject]@{
UserName = 'Default'
Result = $true
Message = 'Removed start2.bin for the default user profile.'
}
}
catch {
$results += [PSCustomObject]@{
UserName = 'Default'
Result = $false
Message = "Failed to remove start2.bin for the default user profile. $($_.Exception.Message)"
}
}
}
}
if ($results.Count -eq 0) {
$results += [PSCustomObject]@{
UserName = 'unknown'
Result = $false
Message = 'No user start menu locations were found.'
}
}
return $results
} }

View File

@@ -10,7 +10,7 @@ function RestartExplorer {
foreach ($paramKey in $script:Params.Keys) { foreach ($paramKey in $script:Params.Keys) {
if ($script:Features.ContainsKey($paramKey) -and $script:Features[$paramKey].RequiresReboot -eq $true) { if ($script:Features.ContainsKey($paramKey) -and $script:Features[$paramKey].RequiresReboot -eq $true) {
$feature = $script:Features[$paramKey] $feature = $script:Features[$paramKey]
Write-Host "Warning: '$($feature.Action) $($feature.Label)' requires a reboot to take full effect" -ForegroundColor Yellow Write-Host "Warning: '$($feature.Label)' requires a reboot to take full effect" -ForegroundColor Yellow
} }
} }

View File

@@ -0,0 +1,241 @@
function Invoke-WithLoadedRestoreHive {
param(
[Parameter(Mandatory)]
[string]$Target,
[Parameter(Mandatory)]
[scriptblock]$ScriptBlock,
$ArgumentObject = $null
)
$hiveDatPath = if ($Target -eq 'DefaultUserProfile') {
GetUserDirectory -userName 'Default' -fileName 'NTUSER.DAT'
}
elseif ($Target -like 'User:*') {
$userName = $Target.Substring(5)
if ([string]::IsNullOrWhiteSpace($userName)) {
throw 'Invalid backup target format for user restore.'
}
GetUserDirectory -userName $userName -fileName 'NTUSER.DAT'
}
else {
throw "Unsupported backup target '$Target'."
}
$global:LASTEXITCODE = 0
reg load 'HKU\Default' "$hiveDatPath" | Out-Null
$loadExitCode = $LASTEXITCODE
if ($loadExitCode -ne 0) {
throw "Failed to load target user hive '$hiveDatPath' (exit code: $loadExitCode)."
}
try {
& $ScriptBlock $ArgumentObject
}
finally {
$global:LASTEXITCODE = 0
reg unload 'HKU\Default' | Out-Null
$unloadExitCode = $LASTEXITCODE
if ($unloadExitCode -ne 0) {
throw "Failed to unload registry hive 'HKU\Default' (exit code: $unloadExitCode)"
}
}
}
function Restore-RegistryKeySnapshot {
param(
[Parameter(Mandatory)]
$Snapshot
)
$registryParts = Split-RegistryPath -path $Snapshot.Path
if (-not $registryParts) {
throw "Unsupported registry path in backup: $($Snapshot.Path)"
}
$rootKey = Get-RegistryRootKey -hiveName $registryParts.Hive
if (-not $rootKey) {
throw "Unsupported registry hive in backup: $($registryParts.Hive)"
}
$subKeyPath = $registryParts.SubKey
if ([string]::IsNullOrWhiteSpace($subKeyPath)) {
throw "Unsupported root-level registry path in backup: $($Snapshot.Path)"
}
if (-not $Snapshot.Exists) {
Remove-RegistrySubKeyTreeIfExists -RootKey $rootKey -SubKeyPath $subKeyPath
return
}
$forceFullTree = @($Snapshot.SubKeys).Count -gt 0
if ($forceFullTree) {
Remove-RegistrySubKeyTreeIfExists -RootKey $rootKey -SubKeyPath $subKeyPath
}
$key = $rootKey.CreateSubKey($subKeyPath)
if ($null -eq $key) {
throw "Unable to create or open registry key '$($Snapshot.Path)'"
}
try {
foreach ($valueSnapshot in @($Snapshot.Values)) {
Restore-RegistryValueSnapshot -RegistryKey $key -Snapshot $valueSnapshot
}
}
finally {
$key.Close()
}
foreach ($subKeySnapshot in @($Snapshot.SubKeys)) {
Restore-RegistryKeySnapshot -Snapshot $subKeySnapshot
}
}
function Restore-RegistryValueSnapshot {
param(
[Parameter(Mandatory)]
[Microsoft.Win32.RegistryKey]$RegistryKey,
[Parameter(Mandatory)]
$Snapshot
)
$valueName = if ($null -ne $Snapshot.Name) { [string]$Snapshot.Name } else { '' }
if (-not [bool]$Snapshot.Exists) {
try {
$RegistryKey.DeleteValue($valueName, $false)
}
catch {
throw "Failed deleting registry value '$valueName' in '$($RegistryKey.Name)': $($_.Exception.Message)"
}
return
}
$valueKind = Convert-RegistryValueKindFromBackup -KindName $Snapshot.Kind
$normalizedData = Convert-RegistryValueDataFromBackup -Kind $valueKind -Data $Snapshot.Data
try {
$RegistryKey.SetValue($valueName, $normalizedData, $valueKind)
}
catch {
$retryBytes = Convert-BackupDataToByteArray -Data $Snapshot.Data
if ($null -ne $retryBytes) {
try {
$RegistryKey.SetValue($valueName, $retryBytes, [Microsoft.Win32.RegistryValueKind]::Binary)
return
}
catch {
# Fall through to original error message for context.
}
}
throw "Failed setting registry value '$valueName' in '$($RegistryKey.Name)': $($_.Exception.Message)"
}
}
function Convert-RegistryValueKindFromBackup {
param(
[string]$KindName
)
if ([string]::IsNullOrWhiteSpace($KindName)) {
return [Microsoft.Win32.RegistryValueKind]::String
}
try {
return [System.Enum]::Parse([Microsoft.Win32.RegistryValueKind], $KindName, $true)
}
catch {
throw "Unsupported registry value kind in backup: $KindName"
}
}
function Convert-RegistryValueDataFromBackup {
param(
[Microsoft.Win32.RegistryValueKind]$Kind,
$Data
)
switch ($Kind) {
([Microsoft.Win32.RegistryValueKind]::DWord) {
$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]::Binary) {
$bytes = Convert-BackupDataToByteArray -Data $Data
if ($null -eq $bytes) {
return (New-Object byte[] 0)
}
return $bytes
}
([Microsoft.Win32.RegistryValueKind]::None) { return $null }
default {
if ($null -ne $Data) {
return [string]$Data
}
return ''
}
}
}
function Convert-BackupDataToByteArray {
param(
$Data
)
if ($null -eq $Data) {
return $null
}
if ($Data -is [byte[]]) {
return ,$Data
}
$items = @($Data)
if ($items.Count -eq 0) {
return ,(New-Object byte[] 0)
}
foreach ($item in $items) {
if ($item -isnot [ValueType] -and $item -isnot [string]) {
return $null
}
$parsed = 0
if (-not [int]::TryParse([string]$item, [ref]$parsed)) {
return $null
}
if ($parsed -lt 0 -or $parsed -gt 255) {
return $null
}
}
$bytes = New-Object byte[] $items.Count
for ($i = 0; $i -lt $items.Count; $i++) {
$bytes[$i] = [byte][int]$items[$i]
}
return ,$bytes
}
function Remove-RegistrySubKeyTreeIfExists {
param(
[Parameter(Mandatory)]
[Microsoft.Win32.RegistryKey]$RootKey,
[Parameter(Mandatory)]
[string]$SubKeyPath
)
$existing = $RootKey.OpenSubKey($SubKeyPath, $false)
if ($existing) {
$existing.Close()
$RootKey.DeleteSubKeyTree($SubKeyPath, $false)
}
}

View File

@@ -0,0 +1,145 @@
function Load-RegistryBackupFromFile {
param(
[Parameter(Mandatory)]
[string]$FilePath
)
if (-not (Test-Path -LiteralPath $FilePath)) {
throw "Backup file was not found: $FilePath"
}
try {
$rawBackup = Get-Content -LiteralPath $FilePath -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop
}
catch {
throw "Failed to read backup file '$FilePath'. The file is not valid JSON."
}
return Normalize-RegistryBackup -Backup $rawBackup
}
function Normalize-RegistryBackup {
param(
[Parameter(Mandatory)]
$Backup
)
$errors = New-Object System.Collections.Generic.List[string]
if (-not $Backup.PSObject.Properties['Version']) {
$errors.Add('Missing property: Version')
}
elseif ([string]$Backup.Version -ne '1.0') {
$errors.Add("Unsupported backup version '$($Backup.Version)'.")
}
if (-not $Backup.PSObject.Properties['BackupType']) {
$errors.Add('Missing property: BackupType')
}
elseif ([string]$Backup.BackupType -ne 'RegistryState') {
$errors.Add("Unsupported BackupType '$($Backup.BackupType)'.")
}
$normalizedTarget = ''
if (-not $Backup.PSObject.Properties['Target'] -or [string]::IsNullOrWhiteSpace([string]$Backup.Target)) {
$errors.Add('Missing property: Target')
}
else {
$normalizedTarget = [string]$Backup.Target
if ($normalizedTarget -eq 'DefaultUserProfile') {
# Valid target format.
}
elseif ($normalizedTarget -like 'User:*') {
$targetUserName = $normalizedTarget.Substring(5)
$targetValidation = Test-TargetUserName -UserName $targetUserName
if (-not $targetValidation.IsValid) {
$errors.Add("Invalid user '$normalizedTarget'")
}
}
elseif ($normalizedTarget -like 'CurrentUser:*') {
$targetCurrentUserName = $normalizedTarget.Substring(12)
if ([string]::IsNullOrWhiteSpace($targetCurrentUserName) -or ($targetCurrentUserName -ne $env:USERNAME)) {
$errors.Add("Backup was made for '$targetCurrentUserName', this does not match current user '$env:USERNAME'.")
}
}
else {
$errors.Add("Unsupported Target '$normalizedTarget'.")
}
}
$registryKeys = @()
if (-not $Backup.PSObject.Properties['RegistryKeys']) {
$errors.Add('Missing property: RegistryKeys')
}
else {
$registryKeys = @($Backup.RegistryKeys)
}
$normalizedKeys = @()
foreach ($keySnapshot in $registryKeys) {
$normalizedKeys += @(Normalize-RegistryKeySnapshot -Snapshot $keySnapshot)
}
$selectedFeatureParseResult = Get-NormalizedSelectedFeatureIdsFromBackup -Backup $Backup
$selectedFeatures = @($selectedFeatureParseResult.SelectedFeatures)
foreach ($selectedFeatureParseError in @($selectedFeatureParseResult.Errors)) {
$errors.Add([string]$selectedFeatureParseError)
}
$allowListValidationErrors = @(Test-RegistryBackupMatchesSelectedFeatures -SelectedFeatureIds @($selectedFeatures) -Target $normalizedTarget -RegistryKeys @($normalizedKeys))
foreach ($allowListValidationError in $allowListValidationErrors) {
$errors.Add([string]$allowListValidationError)
}
if ($errors.Count -gt 0) {
Write-Error "Backup validation failed: $($errors -join ' ')"
if ($errors.Count -eq 1) {
throw ("Validation failed: $($errors[0])")
}
else {
throw ("Validation failed with $($errors.Count) errors. See console output for details.")
}
}
return [PSCustomObject]@{
Version = [string]$Backup.Version
BackupType = [string]$Backup.BackupType
CreatedAt = [string]$Backup.CreatedAt
CreatedBy = [string]$Backup.CreatedBy
ComputerName = [string]$Backup.ComputerName
Target = $normalizedTarget
SelectedFeatures = @($selectedFeatures)
RegistryKeys = @($normalizedKeys)
}
}
function Restore-RegistryBackupState {
param(
[Parameter(Mandatory)]
$Backup
)
$friendlyTarget = GetFriendlyRegistryBackupTarget -Target ([string]$Backup.Target)
$restoreAction = {
param($normalizedBackup)
Write-Host "Applying registry restore from $(@($normalizedBackup.RegistryKeys).Count) root snapshot(s)."
foreach ($rootSnapshot in @($normalizedBackup.RegistryKeys)) {
Restore-RegistryKeySnapshot -Snapshot $rootSnapshot
}
}
Write-Host "Starting restore for $friendlyTarget."
if ($Backup.Target -eq 'DefaultUserProfile' -or $Backup.Target -like 'User:*') {
Write-Host "Restore requires loading target user hive."
Invoke-WithLoadedRestoreHive -Target $Backup.Target -ScriptBlock $restoreAction -ArgumentObject $Backup
Write-Host "Restore completed for $friendlyTarget."
return
}
& $restoreAction $Backup
Write-Host "Restore completed for $friendlyTarget."
}

View File

@@ -6,11 +6,14 @@ function SaveToFile {
[hashtable]$Config, [hashtable]$Config,
[Parameter(Mandatory=$true)] [Parameter(Mandatory=$true)]
[string]$FilePath [string]$FilePath,
[Parameter(Mandatory=$false)]
[int]$MaxDepth = 10
) )
try { try {
$Config | ConvertTo-Json -Depth 10 | Set-Content -Path $FilePath -Encoding UTF8 $Config | ConvertTo-Json -Depth $MaxDepth | Set-Content -Path $FilePath -Encoding UTF8
return $true return $true
} }
catch { catch {

View File

@@ -0,0 +1,121 @@
function New-RestoreDialogState {
param(
[string]$Result = 'Cancel',
[string]$SelectedFile = $null,
$Backup = $null
)
return @{ Result = $Result; SelectedFile = $SelectedFile; Backup = $Backup }
}
function Get-RestoreDialogFeatureDefinition {
param(
[string]$FeatureId,
[hashtable]$Features
)
if ([string]::IsNullOrWhiteSpace($FeatureId) -or -not $Features) {
return $null
}
if ($Features.ContainsKey($FeatureId)) {
return $Features[$FeatureId]
}
return $null
}
function Test-RestoreDialogFeatureCanAutoRevert {
param(
[string]$FeatureId,
[hashtable]$Features
)
if ([string]::IsNullOrWhiteSpace($FeatureId)) {
return $false
}
$featureDefinition = Get-RestoreDialogFeatureDefinition -FeatureId $FeatureId -Features $Features
if ($featureDefinition) {
return -not [string]::IsNullOrWhiteSpace([string]$featureDefinition.RegistryKey)
}
return $false
}
function Get-RestoreDialogFeatureDisplayLabel {
param(
[string]$FeatureId,
[hashtable]$Features
)
if ([string]::IsNullOrWhiteSpace($FeatureId)) {
return 'Unknown feature'
}
$featureDefinition = Get-RestoreDialogFeatureDefinition -FeatureId $FeatureId -Features $Features
if ($featureDefinition -and -not [string]::IsNullOrWhiteSpace([string]$featureDefinition.Label)) {
return [string]$featureDefinition.Label
}
return $FeatureId
}
function Test-RestoreDialogFeatureVisibleInOverview {
param(
[string]$FeatureId,
[hashtable]$Features
)
if ([string]::IsNullOrWhiteSpace($FeatureId)) {
return $false
}
$featureDefinition = Get-RestoreDialogFeatureDefinition -FeatureId $FeatureId -Features $Features
if (-not $featureDefinition) {
return $false
}
return -not [string]::IsNullOrWhiteSpace([string]$featureDefinition.Category)
}
function Get-SelectedFeatureIdsFromBackup {
param($SelectedBackup)
return @(
foreach ($featureId in @($SelectedBackup.SelectedFeatures)) {
if (-not [string]::IsNullOrWhiteSpace([string]$featureId)) {
[string]$featureId
}
}
)
}
function Get-RestoreBackupFeatureLists {
param(
[string[]]$SelectedFeatureIds,
[hashtable]$Features
)
$revertibleFeaturesList = @()
$nonRevertibleFeaturesList = @()
foreach ($featureId in $SelectedFeatureIds) {
if (-not (Test-RestoreDialogFeatureVisibleInOverview -FeatureId $featureId -Features $Features)) {
continue
}
$displayItem = [PSCustomObject]@{ DisplayText = "- $(Get-RestoreDialogFeatureDisplayLabel -FeatureId $featureId -Features $Features)" }
if (Test-RestoreDialogFeatureCanAutoRevert -FeatureId $featureId -Features $Features) {
$revertibleFeaturesList += $displayItem
}
else {
$nonRevertibleFeaturesList += $displayItem
}
}
return [PSCustomObject]@{
Revertible = @($revertibleFeaturesList)
NonRevertible = @($nonRevertibleFeaturesList)
}
}

View File

@@ -75,6 +75,7 @@ function SetWindowThemeResources {
$window.Resources.Add("ButtonPressed", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#3284cc"))) $window.Resources.Add("ButtonPressed", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#3284cc")))
$window.Resources.Add("CloseHover", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#c42b1c"))) $window.Resources.Add("CloseHover", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#c42b1c")))
$window.Resources.Add("InformationIconColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#0078D4"))) $window.Resources.Add("InformationIconColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#0078D4")))
$window.Resources.Add("SuccessIconColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#107C10")))
$window.Resources.Add("WarningIconColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#FFB900"))) $window.Resources.Add("WarningIconColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#FFB900")))
$window.Resources.Add("ErrorIconColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#E81123"))) $window.Resources.Add("ErrorIconColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#E81123")))
$window.Resources.Add("QuestionIconColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#0078D4"))) $window.Resources.Add("QuestionIconColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#0078D4")))

View File

@@ -162,7 +162,7 @@ function Show-ApplyModal {
foreach ($paramKey in $script:Params.Keys) { foreach ($paramKey in $script:Params.Keys) {
if ($script:Features.ContainsKey($paramKey) -and $script:Features[$paramKey].RequiresReboot -eq $true) { if ($script:Features.ContainsKey($paramKey) -and $script:Features[$paramKey].RequiresReboot -eq $true) {
$feature = $script:Features[$paramKey] $feature = $script:Features[$paramKey]
$rebootFeatures += "$($feature.Action) $($feature.Label)" $rebootFeatures += "$($feature.Label)"
} }
} }

View File

@@ -6,7 +6,8 @@ function Show-ImportExportConfigWindow {
[string]$Prompt, [string]$Prompt,
[string[]]$Categories = @('Applications', 'System Tweaks', 'Deployment Settings'), [string[]]$Categories = @('Applications', 'System Tweaks', 'Deployment Settings'),
[string[]]$DisabledCategories = @(), [string[]]$DisabledCategories = @(),
[hashtable]$CategoryDetails = @() [hashtable]$CategoryDetails = @(),
[string]$ActionLabel = 'OK'
) )
# Show overlay on owner window # Show overlay on owner window
@@ -105,6 +106,7 @@ function Show-ImportExportConfigWindow {
$okBtn = $dlg.FindName('OkButton') $okBtn = $dlg.FindName('OkButton')
$cancelBtn = $dlg.FindName('CancelButton') $cancelBtn = $dlg.FindName('CancelButton')
$okBtn.Content = $ActionLabel
$okBtn.Add_Click({ $dlg.Tag = 'OK'; $dlg.Close() }) $okBtn.Add_Click({ $dlg.Tag = 'OK'; $dlg.Close() })
$cancelBtn.Add_Click({ $dlg.Tag = 'Cancel'; $dlg.Close() }) $cancelBtn.Add_Click({ $dlg.Tag = 'Cancel'; $dlg.Close() })
@@ -379,8 +381,11 @@ function Export-Configuration {
$deploymentSettings = Get-DeploymentSettings -Owner $Owner -UserSelectionCombo $UserSelectionCombo -OtherUsernameTextBox $OtherUsernameTextBox $deploymentSettings = Get-DeploymentSettings -Owner $Owner -UserSelectionCombo $UserSelectionCombo -OtherUsernameTextBox $OtherUsernameTextBox
$categoryDetails = Build-CategoryDetails -AppCount $selectedApps.Count -TweakCount $tweakSettings.Count -DeploymentSettings $deploymentSettings $categoryDetails = Build-CategoryDetails -AppCount $selectedApps.Count -TweakCount $tweakSettings.Count -DeploymentSettings $deploymentSettings
$categories = Show-ImportExportConfigWindow -Owner $Owner -UsesDarkMode $UsesDarkMode -Title 'Export Configuration' -Prompt 'Select which settings to include in the export:' -DisabledCategories $disabledCategories -CategoryDetails $categoryDetails $categories = Show-ImportExportConfigWindow -Owner $Owner -UsesDarkMode $UsesDarkMode -Title 'Export Configuration' -Prompt 'Select the settings you wish to include in your export.' -DisabledCategories $disabledCategories -CategoryDetails $categoryDetails -ActionLabel 'Export Settings'
if (-not $categories) { return } if (-not $categories) {
Write-Host 'Export canceled.'
return
}
$config = @{ Version = '1.0' } $config = @{ Version = '1.0' }
@@ -401,12 +406,19 @@ function Export-Configuration {
$saveDialog.DefaultExt = '.json' $saveDialog.DefaultExt = '.json'
$saveDialog.FileName = "Win11Debloat-Config-$(Get-Date -Format 'yyyyMMdd').json" $saveDialog.FileName = "Win11Debloat-Config-$(Get-Date -Format 'yyyyMMdd').json"
if ($saveDialog.ShowDialog($Owner) -ne $true) { return } if ($saveDialog.ShowDialog($Owner) -ne $true) {
Write-Host 'Export save dialog canceled.'
return
}
Write-Host "Exporting configuration to '$($saveDialog.FileName)'... (Categories: $($categories -join ', '))"
if (SaveToFile -Config $config -FilePath $saveDialog.FileName) { if (SaveToFile -Config $config -FilePath $saveDialog.FileName) {
Write-Host "Configuration exported successfully: $($saveDialog.FileName)"
Show-MessageBox -Message "Configuration exported successfully." -Title 'Export Configuration' -Button 'OK' -Icon 'Information' | Out-Null Show-MessageBox -Message "Configuration exported successfully." -Title 'Export Configuration' -Button 'OK' -Icon 'Information' | Out-Null
} }
else { else {
Write-Error "Failed to export configuration to '$($saveDialog.FileName)'"
Show-MessageBox -Message "Failed to export configuration" -Title 'Error' -Button 'OK' -Icon 'Error' | Out-Null Show-MessageBox -Message "Failed to export configuration" -Title 'Error' -Button 'OK' -Icon 'Error' | Out-Null
} }
} }
@@ -425,36 +437,49 @@ function Import-Configuration {
# Show native open-file dialog # Show native open-file dialog
$openDialog = New-Object Microsoft.Win32.OpenFileDialog $openDialog = New-Object Microsoft.Win32.OpenFileDialog
$openDialog.Title = 'Import Configuration' $openDialog.Title = 'Select Configuration File'
$openDialog.Filter = 'JSON files (*.json)|*.json|All files (*.*)|*.*' $openDialog.Filter = 'JSON files (*.json)|*.json|All files (*.*)|*.*'
$openDialog.DefaultExt = '.json' $openDialog.DefaultExt = '.json'
if ($openDialog.ShowDialog($Owner) -ne $true) { return } if ($openDialog.ShowDialog($Owner) -ne $true) {
Write-Host 'Import file dialog canceled.'
return
}
Write-Host "Importing configuration from '$($openDialog.FileName)'..."
$config = LoadJsonFile -filePath $openDialog.FileName -expectedVersion '1.0' $config = LoadJsonFile -filePath $openDialog.FileName -expectedVersion '1.0'
if (-not $config) { if (-not $config) {
Show-MessageBox -Message "Failed to read configuration file" -Title 'Error' -Button 'OK' -Icon 'Error' | Out-Null Write-Error "Failed to read configuration file '$($openDialog.FileName)'"
Show-MessageBox -Message "Failed to read configuration file" -Title 'Invalid Config' -Button 'OK' -Icon 'Error' | Out-Null
return return
} }
if (-not $config.Version) { if (-not $config.Version) {
Show-MessageBox -Message "Invalid configuration file format." -Title 'Error' -Button 'OK' -Icon 'Error' | Out-Null Write-Error "Invalid configuration file format: '$($openDialog.FileName)'"
Show-MessageBox -Message "Invalid configuration file format." -Title 'Invalid Config' -Button 'OK' -Icon 'Error' | Out-Null
return return
} }
$availableCategories = Get-AvailableImportExportCategories -Config $config $availableCategories = Get-AvailableImportExportCategories -Config $config
if ($availableCategories.Count -eq 0) { if ($availableCategories.Count -eq 0) {
Show-MessageBox -Message "The configuration file contains no importable data." -Title 'Import Configuration' -Button 'OK' -Icon 'Information' | Out-Null Write-Warning "Configuration file '$($openDialog.FileName)' contains no importable data."
Show-MessageBox -Message "The selected file contains no importable data." -Title 'Invalid Config' -Button 'OK' -Icon 'Error' | Out-Null
return return
} }
Write-Host "Available categories in config: $($availableCategories -join ', ')"
$appCount = @($config.Apps | Where-Object { $_ -is [string] -and -not [string]::IsNullOrWhiteSpace($_) }).Count $appCount = @($config.Apps | Where-Object { $_ -is [string] -and -not [string]::IsNullOrWhiteSpace($_) }).Count
$tweakCount = @($config.Tweaks | Where-Object { $_ -and $_.Name -and $_.Value -eq $true }).Count $tweakCount = @($config.Tweaks | Where-Object { $_ -and $_.Name -and $_.Value -eq $true }).Count
$categoryDetails = Build-CategoryDetails -AppCount $appCount -TweakCount $tweakCount -DeploymentSettings @($config.Deployment) $categoryDetails = Build-CategoryDetails -AppCount $appCount -TweakCount $tweakCount -DeploymentSettings @($config.Deployment)
$categories = Show-ImportExportConfigWindow -Owner $Owner -UsesDarkMode $UsesDarkMode -Title 'Import Configuration' -Prompt 'Select which settings to import:' -Categories $availableCategories -CategoryDetails $categoryDetails $categories = Show-ImportExportConfigWindow -Owner $Owner -UsesDarkMode $UsesDarkMode -Title 'Import Configuration' -Prompt 'Select the settings you wish to import. You can review and modify them before they are applied.' -Categories $availableCategories -CategoryDetails $categoryDetails -ActionLabel 'Import Settings'
if (-not $categories) { return } if (-not $categories) {
Write-Host 'Import canceled.'
return
}
if ($categories -contains 'Applications' -and $config.Apps) { if ($categories -contains 'Applications' -and $config.Apps) {
$appIds = @( $appIds = @(
@@ -464,6 +489,7 @@ function Import-Configuration {
Where-Object { -not [string]::IsNullOrWhiteSpace($_) } Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
) )
Write-Host "Importing $($appIds.Count) app selection(s)."
Apply-ImportedApplications -AppsPanel $AppsPanel -AppIds $appIds Apply-ImportedApplications -AppsPanel $AppsPanel -AppIds $appIds
if ($OnAppsImported) { if ($OnAppsImported) {
@@ -471,12 +497,16 @@ function Import-Configuration {
} }
} }
if ($categories -contains 'System Tweaks' -and $config.Tweaks) { if ($categories -contains 'System Tweaks' -and $config.Tweaks) {
$tweakCount = @($config.Tweaks).Count
Write-Host "Importing $tweakCount tweak(s)."
Apply-ImportedTweakSettings -Owner $Owner -UiControlMappings $UiControlMappings -TweakSettings @($config.Tweaks) Apply-ImportedTweakSettings -Owner $Owner -UiControlMappings $UiControlMappings -TweakSettings @($config.Tweaks)
} }
if ($categories -contains 'Deployment Settings' -and $config.Deployment) { if ($categories -contains 'Deployment Settings' -and $config.Deployment) {
Write-Host 'Importing deployment settings.'
Apply-ImportedDeploymentSettings -Owner $Owner -UserSelectionCombo $UserSelectionCombo -OtherUsernameTextBox $OtherUsernameTextBox -DeploymentSettings @($config.Deployment) Apply-ImportedDeploymentSettings -Owner $Owner -UserSelectionCombo $UserSelectionCombo -OtherUsernameTextBox $OtherUsernameTextBox -DeploymentSettings @($config.Deployment)
} }
Write-Host 'Configuration imported successfully.'
Show-MessageBox -Message "Configuration imported successfully." -Title 'Import Configuration' -Button 'OK' -Icon 'Information' | Out-Null Show-MessageBox -Message "Configuration imported successfully." -Title 'Import Configuration' -Button 'OK' -Icon 'Information' | Out-Null
if ($OnImportCompleted) { if ($OnImportCompleted) {

View File

@@ -96,6 +96,7 @@ function Show-MainWindow {
$menuAbout = $window.FindName('MenuAbout') $menuAbout = $window.FindName('MenuAbout')
$importConfigBtn = $window.FindName('ImportConfigBtn') $importConfigBtn = $window.FindName('ImportConfigBtn')
$exportConfigBtn = $window.FindName('ExportConfigBtn') $exportConfigBtn = $window.FindName('ExportConfigBtn')
$restoreBackupBtn = $window.FindName('RestoreBackupBtn')
$windowStateNormal = [System.Windows.WindowState]::Normal $windowStateNormal = [System.Windows.WindowState]::Normal
$windowStateMaximized = [System.Windows.WindowState]::Maximized $windowStateMaximized = [System.Windows.WindowState]::Maximized
@@ -198,7 +199,10 @@ function Show-MainWindow {
} }
} }
$window.Add_SizeChanged({ & $updateContentMargin }) $window.Add_SizeChanged({
& $updateContentMargin
UpdateTweaksResponsiveColumns
})
$window.Add_StateChanged({ $window.Add_StateChanged({
& $updateWindowChrome & $updateWindowChrome
@@ -231,7 +235,7 @@ function Show-MainWindow {
}) })
$menuLogs.Add_Click({ $menuLogs.Add_Click({
$logsFolder = Join-Path $PSScriptRoot "../../Logs" $logsFolder = Join-Path (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent) 'Logs'
if (Test-Path $logsFolder) { if (Test-Path $logsFolder) {
Start-Process "explorer.exe" -ArgumentList $logsFolder Start-Process "explorer.exe" -ArgumentList $logsFolder
} }
@@ -244,22 +248,45 @@ function Show-MainWindow {
Show-AboutDialog -Owner $window Show-AboutDialog -Owner $window
}) })
# --- Import/Export Configuration ---
$exportConfigBtn.Add_Click({ $exportConfigBtn.Add_Click({
Export-Configuration -Owner $window -UsesDarkMode $usesDarkMode -AppsPanel $appsPanel -UiControlMappings $script:UiControlMappings -UserSelectionCombo $userSelectionCombo -OtherUsernameTextBox $otherUsernameTextBox try {
Export-Configuration -Owner $window -UsesDarkMode $usesDarkMode -AppsPanel $appsPanel -UiControlMappings $script:UiControlMappings -UserSelectionCombo $userSelectionCombo -OtherUsernameTextBox $otherUsernameTextBox
}
catch {
Write-Warning "Export configuration failed: $($_.Exception.Message)"
Show-MessageBox -Owner $window -Message "Unable to open export configuration dialog: $($_.Exception.Message)" -Title 'Export Configuration Failed' -Button 'OK' -Icon 'Error' | Out-Null
}
}) })
$importConfigBtn.Add_Click({ $importConfigBtn.Add_Click({
Import-Configuration -Owner $window -UsesDarkMode $usesDarkMode -AppsPanel $appsPanel -UiControlMappings $script:UiControlMappings -UserSelectionCombo $userSelectionCombo -OtherUsernameTextBox $otherUsernameTextBox -OnAppsImported { UpdateAppSelectionStatus; UpdatePresetStates } -OnImportCompleted { try {
$tabControl.SelectedIndex = 3 Import-Configuration -Owner $window -UsesDarkMode $usesDarkMode -AppsPanel $appsPanel -UiControlMappings $script:UiControlMappings -UserSelectionCombo $userSelectionCombo -OtherUsernameTextBox $otherUsernameTextBox -OnAppsImported { UpdateAppSelectionStatus; UpdatePresetStates } -OnImportCompleted {
UpdateNavigationButtons $tabControl.SelectedIndex = 3
UpdateNavigationButtons
$window.Dispatcher.BeginInvoke([System.Windows.Threading.DispatcherPriority]::Loaded, [action]{ $window.Dispatcher.BeginInvoke([System.Windows.Threading.DispatcherPriority]::Loaded, [action]{
Show-Bubble -TargetControl $reviewChangesBtn -Message 'View the selected changes here' Show-Bubble -TargetControl $reviewChangesBtn -Message 'View the selected changes here'
}) | Out-Null }) | Out-Null
}
}
catch {
Write-Warning "Import configuration failed: $($_.Exception.Message)"
Show-MessageBox -Owner $window -Message "Unable to open import configuration dialog: $($_.Exception.Message)" -Title 'Import Configuration Failed' -Button 'OK' -Icon 'Error' | Out-Null
} }
}) })
if ($restoreBackupBtn) {
$restoreBackupBtn.Add_Click({
try {
Show-RestoreBackupWindow -Owner $window
}
catch {
Write-Warning "Restore backup action failed: $($_.Exception.Message)"
Show-MessageBox -Owner $window -Message "Unable to open restore backup dialog: $($_.Exception.Message)" -Title 'Restore Backup Failed' -Button 'OK' -Icon 'Error' | Out-Null
}
})
}
$closeBtn.Add_Click({ $closeBtn.Add_Click({
$window.Close() $window.Close()
}) })
@@ -349,6 +376,20 @@ function Show-MainWindow {
} }
} }
function ClearTweakSelections {
if (-not $script:UiControlMappings) { return }
foreach ($controlName in $script:UiControlMappings.Keys) {
$control = $window.FindName($controlName)
if ($control -is [System.Windows.Controls.CheckBox]) {
$control.IsChecked = $false
}
elseif ($control -is [System.Windows.Controls.ComboBox]) {
$control.SelectedIndex = 0
}
}
}
function AnimateDropdownArrow { function AnimateDropdownArrow {
param( param(
[System.Windows.Controls.TextBlock]$arrow, [System.Windows.Controls.TextBlock]$arrow,
@@ -504,6 +545,19 @@ function Show-MainWindow {
} }
} }
$appSelectionStatus.Text = "$selectedCount app(s) selected for removal" $appSelectionStatus.Text = "$selectedCount app(s) selected for removal"
if ($appRemovalScopeCombo -and $appRemovalScopeSection -and $appRemovalScopeDescription) {
if ($selectedCount -gt 0) {
if ($userSelectionCombo.SelectedIndex -ne 2) {
$appRemovalScopeCombo.IsEnabled = $true
}
UpdateAppRemovalScopeDescription
}
else {
$appRemovalScopeCombo.IsEnabled = $false
$appRemovalScopeDescription.Text = "No apps selected for removal."
}
}
} }
# Applies a preset by checking/unchecking apps that match the given filter # Applies a preset by checking/unchecking apps that match the given filter
@@ -582,6 +636,8 @@ function Show-MainWindow {
$script:UiControlMappings = @{} $script:UiControlMappings = @{}
$script:CategoryCardMap = @{} $script:CategoryCardMap = @{}
$script:TweaksCompactMode = $null
$script:TweaksCardsMovedFromCol2 = @()
function CreateLabeledCombo($parent, $labelText, $comboName, $items) { function CreateLabeledCombo($parent, $labelText, $comboName, $items) {
# If only 2 items (No Change + one option), use a checkbox instead # If only 2 items (No Change + one option), use a checkbox instead
@@ -825,7 +881,7 @@ function Show-MainWindow {
if ($feature.FeatureId -match '^Disable') { $opt = 'Disable' } elseif ($feature.FeatureId -match '^Enable') { $opt = 'Enable' } if ($feature.FeatureId -match '^Disable') { $opt = 'Disable' } elseif ($feature.FeatureId -match '^Enable') { $opt = 'Enable' }
$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.Action + ' ' + $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
if ($feature.ToolTip) { if ($feature.ToolTip) {
$tipBlock = New-Object System.Windows.Controls.TextBlock $tipBlock = New-Object System.Windows.Controls.TextBlock
@@ -837,7 +893,7 @@ function Show-MainWindow {
try { $lblBorderObj = $window.FindName("$comboName`_LabelBorder") } catch {} try { $lblBorderObj = $window.FindName("$comboName`_LabelBorder") } catch {}
if ($lblBorderObj) { $lblBorderObj.ToolTip = $tipBlock } if ($lblBorderObj) { $lblBorderObj.ToolTip = $tipBlock }
} }
$script:UiControlMappings[$comboName] = @{ Type='feature'; FeatureId = $feature.FeatureId; Action = $feature.Action; Label = $feature.Label; Category = $categoryName } $script:UiControlMappings[$comboName] = @{ Type='feature'; FeatureId = $feature.FeatureId; Label = $feature.Label; Category = $categoryName }
} }
} }
} }
@@ -845,7 +901,7 @@ function Show-MainWindow {
# Build a feature-label lookup so GenerateOverview can resolve feature IDs without reloading JSON # Build a feature-label lookup so GenerateOverview can resolve feature IDs without reloading JSON
$script:FeatureLabelLookup = @{} $script:FeatureLabelLookup = @{}
foreach ($f in $featuresJson.Features) { foreach ($f in $featuresJson.Features) {
$script:FeatureLabelLookup[$f.FeatureId] = $f.Action + ' ' + $f.Label $script:FeatureLabelLookup[$f.FeatureId] = $f.Label
} }
} }
@@ -1242,6 +1298,52 @@ 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')
function UpdateTweaksResponsiveColumns {
if (-not $tweaksGrid -or -not $col0 -or -not $col1 -or -not $col2) { return }
if ($tweaksGrid.ColumnDefinitions.Count -lt 3) { return }
if ($null -eq $script:TweaksCardsMovedFromCol2) { $script:TweaksCardsMovedFromCol2 = @() }
$useTwoColumns = $window.ActualWidth -lt 1200
if ($script:TweaksCompactMode -eq $useTwoColumns) { return }
$script:TweaksCompactMode = $useTwoColumns
if ($useTwoColumns) {
$tweaksGrid.ColumnDefinitions[0].Width = [System.Windows.GridLength]::new(1, [System.Windows.GridUnitType]::Star)
$tweaksGrid.ColumnDefinitions[1].Width = [System.Windows.GridLength]::new(1, [System.Windows.GridUnitType]::Star)
$tweaksGrid.ColumnDefinitions[2].Width = [System.Windows.GridLength]::new(0)
$col2.Visibility = 'Collapsed'
# Move third-column cards once when entering compact mode.
$cardsToMove = @($col2.Children) | Where-Object { $_ -is [System.Windows.UIElement] }
$script:TweaksCardsMovedFromCol2 = @($cardsToMove)
$col2.Children.Clear()
$targetColumns = @($col0, $col1)
foreach ($card in $cardsToMove) {
$target = $targetColumns |
Sort-Object @{Expression={$_.Children.Count}; Ascending=$true}, @{Expression={$targetColumns.IndexOf($_)}; Ascending=$true} |
Select-Object -First 1
$target.Children.Add($card) | Out-Null
}
return
}
$tweaksGrid.ColumnDefinitions[0].Width = [System.Windows.GridLength]::new(1, [System.Windows.GridUnitType]::Star)
$tweaksGrid.ColumnDefinitions[1].Width = [System.Windows.GridLength]::new(1, [System.Windows.GridUnitType]::Star)
$tweaksGrid.ColumnDefinitions[2].Width = [System.Windows.GridLength]::new(1, [System.Windows.GridUnitType]::Star)
$col2.Visibility = 'Visible'
foreach ($card in (@($script:TweaksCardsMovedFromCol2) | Where-Object { $_ -is [System.Windows.UIElement] })) {
if ($col0.Children.Contains($card)) {
$col0.Children.Remove($card) | Out-Null
}
elseif ($col1.Children.Contains($card)) {
$col1.Children.Remove($card) | Out-Null
}
$col2.Children.Add($card) | Out-Null
}
$script:TweaksCardsMovedFromCol2 = @()
}
# Monitor scrollbar visibility and adjust searchbar margin # Monitor scrollbar visibility and adjust searchbar margin
$tweaksScrollViewer.Add_ScrollChanged({ $tweaksScrollViewer.Add_ScrollChanged({
@@ -1444,8 +1546,6 @@ function Show-MainWindow {
# Show "Current user only" option, hide "Target user only" option # Show "Current user only" option, hide "Target user only" option
$appRemovalScopeCurrentUser.Visibility = 'Visible' $appRemovalScopeCurrentUser.Visibility = 'Visible'
$appRemovalScopeTargetUser.Visibility = 'Collapsed' $appRemovalScopeTargetUser.Visibility = 'Collapsed'
# Enable app removal scope selection for current user
$appRemovalScopeCombo.IsEnabled = $true
$appRemovalScopeCombo.SelectedIndex = 0 $appRemovalScopeCombo.SelectedIndex = 0
} }
1 { 1 {
@@ -1455,8 +1555,6 @@ function Show-MainWindow {
# Hide "Current user only" option, show "Target user only" option # Hide "Current user only" option, show "Target user only" option
$appRemovalScopeCurrentUser.Visibility = 'Collapsed' $appRemovalScopeCurrentUser.Visibility = 'Collapsed'
$appRemovalScopeTargetUser.Visibility = 'Visible' $appRemovalScopeTargetUser.Visibility = 'Visible'
# Enable app removal scope selection for other user
$appRemovalScopeCombo.IsEnabled = $true
$appRemovalScopeCombo.SelectedIndex = 0 $appRemovalScopeCombo.SelectedIndex = 0
} }
2 { 2 {
@@ -1467,10 +1565,12 @@ function Show-MainWindow {
$appRemovalScopeCurrentUser.Visibility = 'Collapsed' $appRemovalScopeCurrentUser.Visibility = 'Collapsed'
$appRemovalScopeTargetUser.Visibility = 'Collapsed' $appRemovalScopeTargetUser.Visibility = 'Collapsed'
# Lock app removal scope to "All users" when applying to sysprep # Lock app removal scope to "All users" when applying to sysprep
$appRemovalScopeCombo.IsEnabled = $false
$appRemovalScopeCombo.SelectedIndex = 0 $appRemovalScopeCombo.SelectedIndex = 0
} }
} }
# Keep enabled/disabled state in sync with both app selection and user mode.
UpdateAppSelectionStatus
}) })
# Helper function to update app removal scope description # Helper function to update app removal scope description
@@ -1513,38 +1613,16 @@ function Show-MainWindow {
return $true return $true
} }
$username = $otherUsernameTextBox.Text.Trim()
$errorBrush = $window.Resources['ValidationErrorColor'] $errorBrush = $window.Resources['ValidationErrorColor']
$successBrush = $window.Resources['ValidationSuccessColor'] $successBrush = $window.Resources['ValidationSuccessColor']
$validationResult = Test-TargetUserName -UserName $otherUsernameTextBox.Text
if ($username.Length -eq 0) { $usernameValidationMessage.Text = $validationResult.Message
$usernameValidationMessage.Text = "Please enter a username" if ($validationResult.IsValid) {
$usernameValidationMessage.Foreground = $errorBrush
return $false
}
if ($username -eq $env:USERNAME) {
$usernameValidationMessage.Text = "Cannot enter your own username, use 'Current User' option instead"
$usernameValidationMessage.Foreground = $errorBrush
return $false
}
$userExists = CheckIfUserExists -Username $username
if ($userExists) {
if (TestIfUserIsLoggedIn -Username $username) {
$usernameValidationMessage.Text = "User '$username' is currently logged in. Please sign out that user first."
$usernameValidationMessage.Foreground = $errorBrush
return $false
}
$usernameValidationMessage.Text = "User found: $username"
$usernameValidationMessage.Foreground = $successBrush $usernameValidationMessage.Foreground = $successBrush
return $true return $true
} }
$usernameValidationMessage.Text = "User not found, please enter a valid username"
$usernameValidationMessage.Foreground = $errorBrush $usernameValidationMessage.Foreground = $errorBrush
return $false return $false
} }
@@ -1563,21 +1641,7 @@ function Show-MainWindow {
$changesList += "Remove $selectedAppsCount application(s)" $changesList += "Remove $selectedAppsCount application(s)"
} }
# Update app removal scope section based on whether apps are selected UpdateAppSelectionStatus
if ($selectedAppsCount -gt 0) {
# Enable app removal scope selection (unless locked by sysprep mode)
if ($userSelectionCombo.SelectedIndex -ne 2) {
$appRemovalScopeCombo.IsEnabled = $true
}
$appRemovalScopeSection.Opacity = 1.0
UpdateAppRemovalScopeDescription
}
else {
# Disable app removal scope selection when no apps selected
$appRemovalScopeCombo.IsEnabled = $false
$appRemovalScopeSection.Opacity = 0.5
$appRemovalScopeDescription.Text = "No apps selected for removal."
}
# Collect all ComboBox/CheckBox selections from dynamically created controls # Collect all ComboBox/CheckBox selections from dynamically created controls
if ($script:UiControlMappings) { if ($script:UiControlMappings) {
@@ -1605,7 +1669,7 @@ function Show-MainWindow {
} }
elseif ($mapping.Type -eq 'feature') { elseif ($mapping.Type -eq 'feature') {
$label = $script:FeatureLabelLookup[$mapping.FeatureId] $label = $script:FeatureLabelLookup[$mapping.FeatureId]
if (-not $label) { $label = $mapping.Action + ' ' + $mapping.Label } if (-not $label) { $label = $mapping.Label }
$changesList += $label $changesList += $label
} }
} }
@@ -1830,6 +1894,7 @@ function Show-MainWindow {
# Initialize UI elements on window load # Initialize UI elements on window load
$window.Add_Loaded({ $window.Add_Loaded({
BuildDynamicTweaks BuildDynamicTweaks
UpdateTweaksResponsiveColumns
RefreshTweakPresetSources -defaultSettingsJson $defaultsJson -lastUsedSettingsJson $lastUsedSettingsJson RefreshTweakPresetSources -defaultSettingsJson $defaultsJson -lastUsedSettingsJson $lastUsedSettingsJson
RegisterTweakPresetControlStateHandlers RegisterTweakPresetControlStateHandlers
UpdateTweakPresetStates UpdateTweakPresetStates
@@ -2159,18 +2224,7 @@ 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({
# Reset all ComboBoxes to index 0 (No Change) and uncheck all CheckBoxes ClearTweakSelections
if ($script:UiControlMappings) {
foreach ($comboName in $script:UiControlMappings.Keys) {
$control = $window.FindName($comboName)
if ($control -is [System.Windows.Controls.CheckBox]) {
$control.IsChecked = $false
}
elseif ($control -is [System.Windows.Controls.ComboBox]) {
$control.SelectedIndex = 0
}
}
}
UpdateTweakPresetStates UpdateTweakPresetStates
}) })

View File

@@ -12,7 +12,7 @@ function Show-MessageBox {
[string]$Button = 'OK', [string]$Button = 'OK',
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[ValidateSet('None', 'Information', 'Warning', 'Error', 'Question')] [ValidateSet('None', 'Information', 'Success', 'Warning', 'Error', 'Question')]
[string]$Icon = 'None', [string]$Icon = 'None',
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
@@ -90,6 +90,11 @@ function Show-MessageBox {
$iconText.Foreground = $msgWindow.FindResource('InformationIconColor') $iconText.Foreground = $msgWindow.FindResource('InformationIconColor')
$iconText.Visibility = 'Visible' $iconText.Visibility = 'Visible'
} }
'Success' {
$iconText.Text = [char]0xE73E
$iconText.Foreground = $msgWindow.FindResource('SuccessIconColor')
$iconText.Visibility = 'Visible'
}
'Warning' { 'Warning' {
$iconText.Text = [char]0xE7BA $iconText.Text = [char]0xE7BA
$iconText.Foreground = $msgWindow.FindResource('WarningIconColor') $iconText.Foreground = $msgWindow.FindResource('WarningIconColor')

View File

@@ -0,0 +1,403 @@
function Show-RestoreBackupDialog {
param(
[Parameter(Mandatory = $false)]
[System.Windows.Window]$Owner = $null
)
Add-Type -AssemblyName PresentationFramework,PresentationCore,WindowsBase | Out-Null
$usesDarkMode = GetSystemUsesDarkMode
$ownerWindow = if ($Owner) { $Owner } else { $script:GuiWindow }
$overlay = $null
$overlayWasAlreadyVisible = $false
if ($ownerWindow) {
try {
$overlay = $ownerWindow.FindName('ModalOverlay')
if ($overlay) {
$overlayWasAlreadyVisible = ($overlay.Visibility -eq 'Visible')
if (-not $overlayWasAlreadyVisible) {
$ownerWindow.Dispatcher.Invoke([action]{ $overlay.Visibility = 'Visible' })
}
}
}
catch { }
}
$schemaPath = $script:RestoreBackupWindowSchema
if (-not $schemaPath -or -not (Test-Path $schemaPath)) {
throw 'Restore backup window schema file could not be found.'
}
$xaml = Get-Content -Path $schemaPath -Raw
$reader = [System.Xml.XmlReader]::Create([System.IO.StringReader]::new($xaml))
try {
$window = [System.Windows.Markup.XamlReader]::Load($reader)
}
finally {
$reader.Close()
}
if ($ownerWindow) {
try {
$window.Owner = $ownerWindow
}
catch { }
}
try {
SetWindowThemeResources -window $window -usesDarkMode $usesDarkMode
}
catch { }
$titleBar = $window.FindName('TitleBar')
$titleText = $window.FindName('TitleText')
$closeBtn = $window.FindName('CloseBtn')
$backBtn = $window.FindName('BackBtn')
$primaryActionBtn = $window.FindName('PrimaryActionBtn')
$chooseRegistryBtn = $window.FindName('ChooseRegistryBtn')
$chooseStartMenuBtn = $window.FindName('ChooseStartMenuBtn')
$restoreModeTabs = $window.FindName('RestoreModeTabs')
$startMenuIntroPanel = $window.FindName('StartMenuIntroPanel')
$startMenuScopeCombo = $window.FindName('StartMenuScopeCombo')
$startMenuAutoBackupCheck = $window.FindName('StartMenuAutoBackupCheck')
$introInfoPanel = $window.FindName('IntroInfoPanel')
$overviewPanel = $window.FindName('OverviewPanel')
$overviewFeaturesSection = $window.FindName('OverviewFeaturesSection')
$overviewSummaryText = $window.FindName('OverviewSummaryText')
$backupFileText = $window.FindName('BackupFileText')
$backupCreatedText = $window.FindName('BackupCreatedText')
$backupTargetText = $window.FindName('BackupTargetText')
$featuresItemsControl = $window.FindName('FeaturesItemsControl')
$nonRevertibleSeparator = $window.FindName('NonRevertibleSeparator')
$nonRevertiblePanel = $window.FindName('NonRevertiblePanel')
$nonRevertibleFeaturesItemsControl = $window.FindName('NonRevertibleFeaturesItemsControl')
$nonRevertibleWikiLink = $window.FindName('NonRevertibleWikiLink')
$titleBar.Add_MouseLeftButtonDown({ $window.DragMove() })
$window.Tag = New-RestoreDialogState
$chooseRegistryBtn.IsDefault = $true
$state = @{ WizardStep = 'SelectType'; SelectedRegistryBackup = $null; SelectedStartMenuBackupFilePath = $null }
$getStartMenuScopeInfo = {
$isAllUsersScope = ($startMenuScopeCombo.SelectedItem.Tag -eq 'AllUsers')
$scopeValue = if ($isAllUsersScope) { 'AllUsers' } else { 'CurrentUser' }
$summaryScopeText = if ($isAllUsersScope) { 'all users' } else { 'the current user' }
return [PSCustomObject]@{
Scope = $scopeValue
Target = $scopeValue
SummaryText = $summaryScopeText
}
}
$showStartMenuIntroState = {
$backupFileText.Text = 'Not selected'
$backupCreatedText.Text = 'N/A'
$overviewSummaryText.Visibility = 'Collapsed'
$overviewPanel.Visibility = 'Collapsed'
$startMenuIntroPanel.Visibility = 'Visible'
$restoreModeTabs.SelectedIndex = 2
}
$showStartMenuOverviewState = {
param([string]$BackupFilePath)
$scopeInfo = & $getStartMenuScopeInfo
$backupTargetText.Text = GetFriendlyRegistryBackupTarget -Target $scopeInfo.Target
$overviewSummaryText.Text = "This will replace the current Start Menu pinned apps layout for $($scopeInfo.SummaryText) with the selected backup."
$backupFileText.Text = Split-Path -Path $BackupFilePath -Leaf
$createdText = 'Unknown'
try {
$createdText = (Get-Item -LiteralPath $BackupFilePath -ErrorAction Stop).LastWriteTime.ToString('yyyy-MM-dd HH:mm')
}
catch { }
$backupCreatedText.Text = $createdText
$overviewFeaturesSection.Visibility = 'Collapsed'
$overviewSummaryText.Visibility = 'Visible'
$nonRevertibleSeparator.Visibility = 'Collapsed'
$nonRevertiblePanel.Visibility = 'Collapsed'
$introInfoPanel.Visibility = 'Collapsed'
$overviewPanel.Visibility = 'Visible'
$restoreModeTabs.SelectedIndex = 1
}
$updateStartMenuOverviewPanel = {
if ($state.WizardStep -ne 'StartMenu') {
return
}
if ([string]::IsNullOrWhiteSpace($state.SelectedStartMenuBackupFilePath)) {
& $showStartMenuIntroState
return
}
& $showStartMenuOverviewState $state.SelectedStartMenuBackupFilePath
}
$updateStartMenuPrimaryActionText = {
if ($state.WizardStep -ne 'StartMenu') {
return
}
$isAutoBackupEnabled = ($startMenuAutoBackupCheck.IsChecked -eq $true)
$hasSelectedManualFile = -not [string]::IsNullOrWhiteSpace($state.SelectedStartMenuBackupFilePath)
if ($isAutoBackupEnabled -or $hasSelectedManualFile) {
$primaryActionBtn.Content = 'Restore backup'
}
else {
$primaryActionBtn.Content = 'Select backup file'
}
}
$refreshStartMenuUi = {
& $updateStartMenuOverviewPanel
& $updateStartMenuPrimaryActionText
}
$enterSelectTypeStep = {
$titleText.Text = 'Restore Backup'
$restoreModeTabs.SelectedIndex = 0
$backBtn.Visibility = 'Visible'
$backBtn.Content = 'Cancel'
$primaryActionBtn.Visibility = 'Collapsed'
$chooseRegistryBtn.IsDefault = $true
$primaryActionBtn.IsDefault = $false
}
$enterRegistryStep = {
$titleText.Text = 'Restore Registry Backup'
$restoreModeTabs.SelectedIndex = 1
$introInfoPanel.Visibility = 'Visible'
$overviewPanel.Visibility = 'Collapsed'
$overviewFeaturesSection.Visibility = 'Visible'
$overviewSummaryText.Visibility = 'Collapsed'
$backBtn.Visibility = 'Visible'
$backBtn.Content = 'Back'
$primaryActionBtn.Visibility = 'Visible'
$primaryActionBtn.Content = 'Select backup file'
$primaryActionBtn.IsDefault = $true
$chooseRegistryBtn.IsDefault = $false
}
$enterStartMenuStep = {
$titleText.Text = 'Restore Start Menu Backup'
$restoreModeTabs.SelectedIndex = 2
$backBtn.Visibility = 'Visible'
$backBtn.Content = 'Back'
$primaryActionBtn.Visibility = 'Visible'
$primaryActionBtn.IsDefault = $true
$chooseRegistryBtn.IsDefault = $false
& $refreshStartMenuUi
}
$showRegistryOverview = {
param(
[Parameter(Mandatory = $true)]
$SelectedBackup,
[Parameter(Mandatory = $true)]
[string]$SelectedBackupFilePath
)
$createdText = if ([string]::IsNullOrWhiteSpace($SelectedBackup.CreatedAt)) {
'Unknown'
}
else {
try {
[DateTime]::Parse($SelectedBackup.CreatedAt).ToString('yyyy-MM-dd HH:mm')
}
catch {
$SelectedBackup.CreatedAt
}
}
$selectedFeatureIds = Get-SelectedFeatureIdsFromBackup -SelectedBackup $SelectedBackup
$featureLists = Get-RestoreBackupFeatureLists -SelectedFeatureIds $selectedFeatureIds -Features $script:Features
$revertibleFeaturesList = @($featureLists.Revertible)
$nonRevertibleFeaturesList = @($featureLists.NonRevertible)
Write-Host "Backup overview prepared. Revertible=$($revertibleFeaturesList.Count), NonRevertible=$($nonRevertibleFeaturesList.Count)"
if ($revertibleFeaturesList.Count -eq 0) {
throw 'The selected backup does not contain any changes that can be restored.'
}
$backupFileText.Text = Split-Path $SelectedBackupFilePath -Leaf
$backupCreatedText.Text = $createdText
$backupTargetText.Text = GetFriendlyRegistryBackupTarget -Target ([string]$SelectedBackup.Target)
$featuresItemsControl.ItemsSource = $revertibleFeaturesList
$overviewFeaturesSection.Visibility = 'Visible'
$overviewSummaryText.Visibility = 'Collapsed'
$nonRevertibleFeaturesItemsControl.ItemsSource = $nonRevertibleFeaturesList
$hasNonRevertibleItems = ($nonRevertibleFeaturesList.Count -gt 0)
if ($hasNonRevertibleItems) { $nonRevertiblePanel.Visibility = 'Visible' } else { $nonRevertiblePanel.Visibility = 'Collapsed' }
if ($hasNonRevertibleItems) { $nonRevertibleSeparator.Visibility = 'Visible' } else { $nonRevertibleSeparator.Visibility = 'Collapsed' }
$introInfoPanel.Visibility = 'Collapsed'
$overviewPanel.Visibility = 'Visible'
return $true
}
$handleRegistryPrimaryAction = {
if ($state.SelectedRegistryBackup) {
$window.Tag = @{
Result = 'RestoreRegistry'
Backup = $state.SelectedRegistryBackup
}
$window.DialogResult = $true
$window.Close()
return
}
$openDialog = New-Object Microsoft.Win32.OpenFileDialog
$openDialog.Title = 'Select Registry Backup File'
$openDialog.Filter = 'Registry backup (*.json)|*.json|All files (*.*)|*.*'
$openDialog.DefaultExt = '.json'
$openDialog.InitialDirectory = $script:RegistryBackupsPath
if ($openDialog.ShowDialog($window) -ne $true) {
return
}
Write-Host "Backup file selected: $($openDialog.FileName)"
$selectedBackup = Load-RegistryBackupFromFile -FilePath $openDialog.FileName
if (-not (& $showRegistryOverview -SelectedBackup $selectedBackup -SelectedBackupFilePath $openDialog.FileName)) {
return
}
$state.SelectedRegistryBackup = $selectedBackup
$primaryActionBtn.Content = 'Restore from backup'
}
$handleStartMenuPrimaryAction = {
$scope = (& $getStartMenuScopeInfo).Scope
$useManualBackupFile = -not ($startMenuAutoBackupCheck.IsChecked -eq $true)
if ($useManualBackupFile -and [string]::IsNullOrWhiteSpace($state.SelectedStartMenuBackupFilePath)) {
$openDialog = New-Object Microsoft.Win32.OpenFileDialog
$openDialog.Title = 'Select Start Menu Backup File'
$openDialog.Filter = 'Start Menu backup (*.bak)|*.bak'
$openDialog.InitialDirectory = "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState"
$openDialog.DefaultExt = '.bak'
if ($openDialog.ShowDialog($window) -ne $true) {
return
}
$state.SelectedStartMenuBackupFilePath = $openDialog.FileName
Write-Host "Selected Start Menu backup file: $($state.SelectedStartMenuBackupFilePath)"
& $refreshStartMenuUi
return
}
$window.Tag = @{
Result = 'RestoreStartMenu'
StartMenuScope = $scope
UseManualBackupFile = $useManualBackupFile
BackupFilePath = $state.SelectedStartMenuBackupFilePath
}
$window.DialogResult = $true
$window.Close()
}
$setWizardStep = {
param([string]$step)
$state.WizardStep = $step
switch ($step) {
'SelectType' { & $enterSelectTypeStep }
'Registry' { & $enterRegistryStep }
'StartMenu' { & $enterStartMenuStep }
}
}
$startMenuAutoBackupCheck.Add_Checked({
$state.SelectedStartMenuBackupFilePath = $null
& $refreshStartMenuUi
})
$startMenuAutoBackupCheck.Add_Unchecked({
& $refreshStartMenuUi
})
$startMenuScopeCombo.Add_SelectionChanged({
& $refreshStartMenuUi
})
$nonRevertibleWikiLink.Add_MouseLeftButtonUp({
try {
Start-Process 'https://github.com/Raphire/Win11Debloat/wiki/Reverting-Changes' | Out-Null
}
catch { }
})
$closeBtn.Add_Click({
$window.Tag = New-RestoreDialogState
$window.DialogResult = $false
$window.Close()
})
$chooseRegistryBtn.Add_Click({ & $setWizardStep 'Registry' })
$chooseStartMenuBtn.Add_Click({ & $setWizardStep 'StartMenu' })
$backBtn.Add_Click({
if ($state.WizardStep -eq 'SelectType') {
$window.Tag = New-RestoreDialogState
$window.DialogResult = $false
$window.Close()
return
}
if ($state.WizardStep -eq 'Registry') {
$state.SelectedRegistryBackup = $null
}
if ($state.WizardStep -eq 'StartMenu') {
$state.SelectedStartMenuBackupFilePath = $null
$startMenuAutoBackupCheck.IsChecked = $true
}
& $setWizardStep 'SelectType'
})
$primaryActionBtn.Add_Click({
switch ($state.WizardStep) {
'Registry' { & $handleRegistryPrimaryAction }
'StartMenu' { & $handleStartMenuPrimaryAction }
}
})
$window.Add_KeyDown({
param($source, $e)
if ($e.Key -eq 'Escape') {
$window.Tag = New-RestoreDialogState
$window.DialogResult = $false
$window.Close()
}
})
& $setWizardStep 'SelectType'
try {
$null = $window.ShowDialog()
}
catch {
$innerMessage = if ($_.Exception.InnerException) { $_.Exception.InnerException.Message } else { 'None' }
throw "Failed to show restore backup dialog. Error: $($_.Exception.Message) Inner: $innerMessage"
}
finally {
if ($overlay -and -not $overlayWasAlreadyVisible) {
try {
$ownerWindow.Dispatcher.Invoke([action]{ $overlay.Visibility = 'Collapsed' })
}
catch { }
}
}
return $window.Tag
}

View File

@@ -0,0 +1,88 @@
function Show-RestoreBackupWindow {
param(
[Parameter(Mandatory = $false)]
[System.Windows.Window]$Owner = $null
)
try {
Write-Host 'Opening restore backup dialog.'
$dialogResult = Show-RestoreBackupDialog -Owner $Owner
if (-not $dialogResult -or $dialogResult.Result -eq 'Cancel') {
Write-Host 'Restore canceled by user.'
return
}
$successMessage = $null
$warningMessage = $null
if ($dialogResult.Result -eq 'RestoreRegistry') {
$backup = $dialogResult.Backup
if (-not $backup) {
throw 'Registry backup restore requested without a selected backup.'
}
Write-Host "User confirmed registry restore for $($backup.Target)."
Restore-RegistryBackupState -Backup $backup
$successMessage = 'Registry backup restored successfully. Please restart your computer for all changes to take effect.'
}
elseif ($dialogResult.Result -eq 'RestoreStartMenu') {
$scope = $dialogResult.StartMenuScope
$useManualBackupFile = ($dialogResult.UseManualBackupFile -eq $true)
$backupFilePath = $null
if ($dialogResult -is [hashtable] -and $dialogResult.ContainsKey('BackupFilePath')) {
$backupFilePath = $dialogResult['BackupFilePath']
}
elseif ($dialogResult.PSObject.Properties.Match('BackupFilePath').Count -gt 0) {
$backupFilePath = $dialogResult.BackupFilePath
}
if ($useManualBackupFile -and [string]::IsNullOrWhiteSpace($backupFilePath)) {
throw 'Start Menu restore canceled: no backup file selected.'
}
$result = if ($scope -eq 'AllUsers') {
RestoreStartMenuForAllUsers -BackupFilePath $backupFilePath
}
else {
RestoreStartMenu -BackupFilePath $backupFilePath
}
$resultEntries = @($result)
$successCount = @($resultEntries | Where-Object { $_.Result -eq $true }).Count
$failedEntries = @($resultEntries | Where-Object { $_.Result -ne $true })
if ($successCount -eq 0) {
$errorSummary = ($resultEntries | ForEach-Object { $_.Message }) -join [Environment]::NewLine
throw "Failed to restore the Start Menu backup.`n$errorSummary"
}
if ($failedEntries.Count -gt 0) {
$failureSummary = ($failedEntries | ForEach-Object { $_.Message }) -join [Environment]::NewLine
$warningMessage = "The Start Menu backup was successfully restored for $successCount user(s).`nSome users could not be restored:`n$failureSummary"
}
else {
if ($scope -eq 'AllUsers') {
$successMessage = "The Start Menu backup was successfully restored for all users. The changes will apply the next time users sign in."
}
else {
$successMessage = "The Start Menu backup was successfully restored for the current user. The changes will apply the next time you sign in."
}
}
}
if ($warningMessage) {
Write-Host "$warningMessage"
Show-MessageBox -Title 'Backup Restored' -Message $warningMessage -Icon Warning
}
elseif ($successMessage) {
Write-Host "$successMessage"
Show-MessageBox -Title 'Backup Restored' -Message $successMessage -Icon Success
}
}
catch {
$errorMessage = if ($_.Exception.Message) { $_.Exception.Message } else { 'An unexpected error occurred.' }
Write-Error "Restore operation failed: $errorMessage"
Show-MessageBox -Title 'Error' -Message "Restore failed: $errorMessage" -Icon Error
}
}

View File

@@ -117,11 +117,15 @@ Write-Output "------------------------------------------------------------------
Write-Output " Win11Debloat Script - Get Dev" Write-Output " Win11Debloat Script - Get Dev"
Write-Output "-------------------------------------------------------------------------------------------" Write-Output "-------------------------------------------------------------------------------------------"
$tempRootPath = $env:TEMP
$tempWorkPath = Join-Path $tempRootPath 'Win11Debloat'
$tempArchivePath = Join-Path $tempRootPath 'win11debloat.zip'
Write-Output "> Downloading Win11Debloat for development..." Write-Output "> Downloading Win11Debloat for development..."
# Download latest version of Win11Debloat from GitHub master branch as zip archive # Download latest version of Win11Debloat from GitHub master branch as zip archive
try { try {
Invoke-RestMethod "https://github.com/Raphire/Win11Debloat/archive/refs/heads/master.zip" -OutFile "$env:TEMP/win11debloat.zip" Invoke-RestMethod "https://github.com/Raphire/Win11Debloat/archive/refs/heads/master.zip" -OutFile $tempArchivePath
} }
catch { catch {
Write-Host "Error: Unable to fetch master branch from GitHub. Please check your internet connection and try again." -ForegroundColor Red Write-Host "Error: Unable to fetch master branch from GitHub. Please check your internet connection and try again." -ForegroundColor Red
@@ -135,12 +139,12 @@ 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 # Remove old script folder if it exists, but keep config and log files
if (Test-Path "$env:TEMP/Win11Debloat") { if (Test-Path $tempWorkPath) {
Get-ChildItem -Path "$env:TEMP/Win11Debloat" -Exclude CustomAppsList,LastUsedSettings.json,Win11Debloat.log,Config,Logs | Remove-Item -Recurse -Force Get-ChildItem -Path $tempWorkPath -Exclude CustomAppsList,LastUsedSettings.json,Win11Debloat.log,Config,Logs,Backups | Remove-Item -Recurse -Force
} }
$configDir = "$env:TEMP/Win11Debloat/Config" $configDir = Join-Path $tempWorkPath 'Config'
$backupDir = "$env:TEMP/Win11Debloat/ConfigOld" $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") {
@@ -160,13 +164,13 @@ Write-Output ""
Write-Output "> Unpacking..." Write-Output "> Unpacking..."
# Unzip archive to Win11Debloat folder # Unzip archive to Win11Debloat folder
Expand-Archive "$env:TEMP/win11debloat.zip" "$env:TEMP/Win11Debloat" Expand-Archive $tempArchivePath $tempWorkPath
# Remove archive # Remove archive
Remove-Item "$env:TEMP/win11debloat.zip" Remove-Item $tempArchivePath
# Move files # Move files
Get-ChildItem -Path "$env:TEMP/Win11Debloat/*Win11Debloat-*" -Recurse | Move-Item -Destination "$env:TEMP/Win11Debloat" Get-ChildItem -Path (Join-Path $tempWorkPath '*Win11Debloat-*') -Recurse | Move-Item -Destination $tempWorkPath
# Add existing config files back to Config folder # Add existing config files back to Config folder
if (Test-Path "$backupDir") { if (Test-Path "$backupDir") {
@@ -206,7 +210,8 @@ if ($PSVersionTable.PSVersion.Major -ge 7) {
} }
# Run Win11Debloat script with the provided arguments # Run Win11Debloat script with the provided arguments
$debloatProcess = Start-Process powershell.exe -WindowStyle $windowStyle -PassThru -ArgumentList "-executionpolicy bypass -File $env:TEMP\Win11Debloat\Win11Debloat.ps1 $arguments" -Verb RunAs $debloatScriptPath = Join-Path $tempWorkPath 'Win11Debloat.ps1'
$debloatProcess = Start-Process powershell.exe -WindowStyle $windowStyle -PassThru -ArgumentList "-executionpolicy bypass -File `"$debloatScriptPath`" $arguments" -Verb RunAs
# Wait for the process to finish before continuing # Wait for the process to finish before continuing
if ($null -ne $debloatProcess) { if ($null -ne $debloatProcess) {
@@ -214,12 +219,12 @@ if ($null -ne $debloatProcess) {
} }
# Remove all remaining script files, except for CustomAppsList and LastUsedSettings.json files # Remove all remaining script files, except for CustomAppsList and LastUsedSettings.json files
if (Test-Path "$env:TEMP/Win11Debloat") { 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 "$env:TEMP/Win11Debloat" -Exclude CustomAppsList,LastUsedSettings.json,Win11Debloat.log,Win11Debloat-Run.log,Config,Logs | Remove-Item -Recurse -Force Get-ChildItem -Path $tempWorkPath -Exclude CustomAppsList,LastUsedSettings.json,Win11Debloat.log,Win11Debloat-Run.log,Config,Logs,Backups | Remove-Item -Recurse -Force
} }
Write-Output "" Write-Output ""

View File

@@ -117,12 +117,16 @@ Write-Output "------------------------------------------------------------------
Write-Output " Win11Debloat Script - Get" Write-Output " Win11Debloat Script - Get"
Write-Output "-------------------------------------------------------------------------------------------" Write-Output "-------------------------------------------------------------------------------------------"
$tempRootPath = $env:TEMP
$tempWorkPath = Join-Path $tempRootPath 'Win11Debloat'
$tempArchivePath = Join-Path $tempRootPath 'win11debloat.zip'
Write-Output "> Downloading Win11Debloat..." Write-Output "> Downloading Win11Debloat..."
# Download latest version of Win11Debloat from GitHub as zip archive # Download latest version of Win11Debloat from GitHub as zip archive
try { try {
$LatestReleaseUri = (Invoke-RestMethod https://api.github.com/repos/Raphire/Win11Debloat/releases/latest).zipball_url $LatestReleaseUri = (Invoke-RestMethod https://api.github.com/repos/Raphire/Win11Debloat/releases/latest).zipball_url
Invoke-RestMethod $LatestReleaseUri -OutFile "$env:TEMP/win11debloat.zip" Invoke-RestMethod $LatestReleaseUri -OutFile $tempArchivePath
} }
catch { catch {
Write-Host "Error: Unable to fetch latest release from GitHub. Please check your internet connection and try again." -ForegroundColor Red Write-Host "Error: Unable to fetch latest release from GitHub. Please check your internet connection and try again." -ForegroundColor Red
@@ -136,12 +140,12 @@ 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 # Remove old script folder if it exists, but keep config and log files
if (Test-Path "$env:TEMP/Win11Debloat") { if (Test-Path $tempWorkPath) {
Get-ChildItem -Path "$env:TEMP/Win11Debloat" -Exclude CustomAppsList,LastUsedSettings.json,Win11Debloat.log,Config,Logs | Remove-Item -Recurse -Force Get-ChildItem -Path $tempWorkPath -Exclude CustomAppsList,LastUsedSettings.json,Win11Debloat.log,Config,Logs,Backups | Remove-Item -Recurse -Force
} }
$configDir = "$env:TEMP/Win11Debloat/Config" $configDir = Join-Path $tempWorkPath 'Config'
$backupDir = "$env:TEMP/Win11Debloat/ConfigOld" $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") {
@@ -161,13 +165,13 @@ Write-Output ""
Write-Output "> Unpacking..." Write-Output "> Unpacking..."
# Unzip archive to Win11Debloat folder # Unzip archive to Win11Debloat folder
Expand-Archive "$env:TEMP/win11debloat.zip" "$env:TEMP/Win11Debloat" Expand-Archive $tempArchivePath $tempWorkPath
# Remove archive # Remove archive
Remove-Item "$env:TEMP/win11debloat.zip" Remove-Item $tempArchivePath
# Move files # Move files
Get-ChildItem -Path "$env:TEMP/Win11Debloat/Raphire-Win11Debloat-*" -Recurse | Move-Item -Destination "$env:TEMP/Win11Debloat" Get-ChildItem -Path (Join-Path $tempWorkPath 'Raphire-Win11Debloat-*') -Recurse | Move-Item -Destination $tempWorkPath
# Add existing config files back to Config folder # Add existing config files back to Config folder
if (Test-Path "$backupDir") { if (Test-Path "$backupDir") {
@@ -207,7 +211,8 @@ if ($PSVersionTable.PSVersion.Major -ge 7) {
} }
# Run Win11Debloat script with the provided arguments # Run Win11Debloat script with the provided arguments
$debloatProcess = Start-Process powershell.exe -WindowStyle $windowStyle -PassThru -ArgumentList "-executionpolicy bypass -File $env:TEMP\Win11Debloat\Win11Debloat.ps1 $arguments" -Verb RunAs $debloatScriptPath = Join-Path $tempWorkPath 'Win11Debloat.ps1'
$debloatProcess = Start-Process powershell.exe -WindowStyle $windowStyle -PassThru -ArgumentList "-executionpolicy bypass -File `"$debloatScriptPath`" $arguments" -Verb RunAs
# Wait for the process to finish before continuing # Wait for the process to finish before continuing
if ($null -ne $debloatProcess) { if ($null -ne $debloatProcess) {
@@ -215,12 +220,12 @@ if ($null -ne $debloatProcess) {
} }
# Remove all remaining script files, except for CustomAppsList and LastUsedSettings.json files # Remove all remaining script files, except for CustomAppsList and LastUsedSettings.json files
if (Test-Path "$env:TEMP/Win11Debloat") { 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 "$env:TEMP/Win11Debloat" -Exclude CustomAppsList,LastUsedSettings.json,Win11Debloat.log,Win11Debloat-Run.log,Config,Logs | Remove-Item -Recurse -Force Get-ChildItem -Path $tempWorkPath -Exclude CustomAppsList,LastUsedSettings.json,Win11Debloat.log,Win11Debloat-Run.log,Config,Logs,Backups | Remove-Item -Recurse -Force
} }
Write-Output "" Write-Output ""

View 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'."
}
}

View File

@@ -1,31 +1,41 @@
function CheckIfUserExists { function CheckIfUserExists {
param ( param (
$userName [string]$userName
) )
if ($userName -match '[<>:"|?*]') {
return $false
}
if ([string]::IsNullOrWhiteSpace($userName)) { if ([string]::IsNullOrWhiteSpace($userName)) {
return $false return $false
} }
$lookupName = $userName.Trim()
# Validate special characters against the local username segment (user in DOMAIN\user or user@domain).
$localUserName = GetLocalUserNameSegment -UserName $lookupName
if ($localUserName.IndexOfAny([System.IO.Path]::GetInvalidFileNameChars()) -ge 0) {
return $false
}
# PowerShell treats [] as wildcard chars in non-literal paths; disallow them explicitly.
if ($localUserName -match '[\[\]]') {
return $false
}
try { try {
$userExists = Test-Path "$env:SystemDrive\Users\$userName" $userContext = ResolveUserProfileContext -UserName $lookupName
if (-not $userContext -or [string]::IsNullOrWhiteSpace($userContext.ProfilePath)) {
return $false
}
if ($userExists) { if ($lookupName -ieq 'Default') {
return $true return $true
} }
$userExists = Test-Path ($env:USERPROFILE -Replace ('\\' + $env:USERNAME + '$'), "\$userName") return -not [string]::IsNullOrWhiteSpace($userContext.UserSid)
if ($userExists) {
return $true
}
} }
catch { catch {
Write-Error "Something went wrong when trying to find the user directory path for user $userName. Please ensure the user exists on this system" Write-Error "Something went wrong when trying to find the user directory path for user $lookupName. Please ensure the user exists on this system"
} }
return $false return $false

View File

@@ -0,0 +1,174 @@
function Get-RegFileOperations {
param(
[Parameter(Mandatory)]
[string]$regFilePath
)
$content = Get-Content -Path $regFilePath -Raw -ErrorAction Stop
$rawLines = $content -split "`r?`n"
# Join continuation lines (lines ending with \)
$lines = @()
$i = 0
while ($i -lt $rawLines.Count) {
$line = $rawLines[$i]
# Join lines that end with backslash to the next line(s)
while ($line.EndsWith("\") -and $i + 1 -lt $rawLines.Count) {
$line = $line.Substring(0, $line.Length - 1) + $rawLines[$i + 1]
$i++
}
$lines += $line
$i++
}
$operations = @()
$currentKeyPath = $null
$isDeletedKey = $false
foreach ($rawLine in $lines) {
$line = $rawLine.Trim()
if ([string]::IsNullOrWhiteSpace($line) -or $line.StartsWith(';')) {
continue
}
if ($line -match '^Windows Registry Editor Version') {
continue
}
if ($line -match '^\[(?<deleted>-)?(?<keyPath>[^\]]+)\]$') {
$currentKeyPath = $matches.keyPath.Trim()
$isDeletedKey = $matches.deleted -eq '-'
if ($isDeletedKey) {
$operations += [PSCustomObject]@{
OperationType = 'DeleteKey'
KeyPath = $currentKeyPath
}
}
continue
}
if (-not $currentKeyPath -or $isDeletedKey) {
continue
}
if ($line -notmatch '^(?<valueName>@|"[^"]+")=(?<valueData>.*)$') {
continue
}
$valueNameToken = $matches.valueName
$valueName = if ($valueNameToken -eq '@') {
''
}
else {
$valueNameToken.Trim('"')
}
$parsedValue = Convert-RegValueData -valueData $matches.valueData.Trim()
if (-not $parsedValue) { continue }
$operations += [PSCustomObject]@{
OperationType = $parsedValue.OperationType
KeyPath = $currentKeyPath
ValueName = $valueName
ValueType = $parsedValue.ValueType
ValueData = $parsedValue.ValueData
}
}
return $operations
}
function Convert-RegValueData {
param(
[Parameter(Mandatory)]
[string]$valueData
)
if ($valueData -eq '-') {
return [PSCustomObject]@{
OperationType = 'DeleteValue'
ValueType = $null
ValueData = $null
}
}
if ($valueData -match '^dword:(?<value>[0-9a-fA-F]{1,8})$') {
return [PSCustomObject]@{
OperationType = 'SetValue'
ValueType = 'DWord'
ValueData = [uint32]::Parse($matches.value, [System.Globalization.NumberStyles]::HexNumber)
}
}
if ($valueData -match '^qword:(?<value>[0-9a-fA-F]{1,16})$') {
return [PSCustomObject]@{
OperationType = 'SetValue'
ValueType = 'QWord'
ValueData = [uint64]::Parse($matches.value, [System.Globalization.NumberStyles]::HexNumber)
}
}
if ($valueData -match '^hex(?:\((?<kind>[0-9a-fA-F]+)\))?:(?<bytes>[0-9a-fA-F,\s]+)$') {
$bytes = Convert-HexStringToByteArray -hexValue $matches.bytes
$valueType = if ($matches.kind) { "Hex$($matches.kind)" } else { 'Binary' }
$value = switch ($matches.kind) {
'2' { Convert-RegistryByteArrayToString -byteData $bytes }
'7' { Convert-RegistryByteArrayToMultiString -byteData $bytes }
default { $bytes }
}
return [PSCustomObject]@{
OperationType = 'SetValue'
ValueType = $valueType
ValueData = $value
}
}
if ($valueData -match '^"(?<value>.*)"$') {
return [PSCustomObject]@{
OperationType = 'SetValue'
ValueType = 'String'
ValueData = $matches.value
}
}
return $null
}
function Convert-HexStringToByteArray {
param(
[Parameter(Mandatory)]
[string]$hexValue
)
$parts = $hexValue.Split(',') | ForEach-Object { $_.Trim() } | Where-Object { $_ }
$bytes = New-Object byte[] $parts.Count
for ($i = 0; $i -lt $parts.Count; $i++) {
$bytes[$i] = [byte]::Parse($parts[$i], [System.Globalization.NumberStyles]::HexNumber)
}
return $bytes
}
function Convert-RegistryByteArrayToString {
param(
[Parameter(Mandatory)]
[byte[]]$byteData
)
return ([System.Text.Encoding]::Unicode.GetString($byteData)).TrimEnd([char]0)
}
function Convert-RegistryByteArrayToMultiString {
param(
[Parameter(Mandatory)]
[byte[]]$byteData
)
return @(([System.Text.Encoding]::Unicode.GetString($byteData)).TrimEnd([char]0) -split "`0" | Where-Object { $_ -ne '' })
}

View File

@@ -0,0 +1,43 @@
function GetFriendlyRegistryBackupTarget {
param(
[AllowNull()]
[AllowEmptyString()]
[string]$Target
)
if ([string]::IsNullOrWhiteSpace($Target)) {
return 'Unknown'
}
if ($Target -eq 'DefaultUserProfile') {
return 'Default user profile'
}
if ($Target -eq 'CurrentUser') {
return 'Current user'
}
if ($Target -eq 'AllUsers') {
return 'All users'
}
if ($Target -like 'CurrentUser:*') {
$userName = $Target.Substring(12)
if ([string]::IsNullOrWhiteSpace($userName)) {
return 'Current user'
}
return "Current user ($userName)"
}
if ($Target -like 'User:*') {
$userName = $Target.Substring(5)
if ([string]::IsNullOrWhiteSpace($userName)) {
return 'User'
}
return "User ($userName)"
}
return $Target
}

View File

@@ -7,23 +7,41 @@ function GetUserDirectory {
) )
try { try {
if (-not (CheckIfUserExists -userName $userName) -and $userName -ne "*") { if ($userName -eq "*") {
Write-Error "User $userName does not exist on this system" $rootPaths = @(
AwaitKeyToExit (Join-Path $env:SystemDrive 'Users')
(Split-Path -Path $env:USERPROFILE -Parent)
) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique
foreach ($rootPath in $rootPaths) {
if (-not (Test-Path -LiteralPath $rootPath -PathType Container)) {
continue
}
$wildcardPath = if ([string]::IsNullOrWhiteSpace($fileName)) {
Join-Path $rootPath '*'
}
else {
Join-Path (Join-Path $rootPath '*') $fileName
}
return $wildcardPath
}
} }
$userDirectoryExists = Test-Path "$env:SystemDrive\Users\$userName" $userContext = ResolveUserProfileContext -UserName $userName
$userPath = "$env:SystemDrive\Users\$userName\$fileName" $resolvedUserDirectory = if ($userContext) { $userContext.ProfilePath } else { $null }
if ($resolvedUserDirectory) {
$userPath = if ([string]::IsNullOrWhiteSpace($fileName)) {
$resolvedUserDirectory
}
else {
Join-Path $resolvedUserDirectory $fileName
}
if ((Test-Path $userPath) -or ($userDirectoryExists -and (-not $exitIfPathNotFound))) { if ((Test-Path -LiteralPath $userPath) -or ((Test-Path -LiteralPath $resolvedUserDirectory -PathType Container) -and (-not $exitIfPathNotFound))) {
return $userPath return $userPath
} }
$userDirectoryExists = Test-Path ($env:USERPROFILE -Replace ('\\' + $env:USERNAME + '$'), "\$userName")
$userPath = $env:USERPROFILE -Replace ('\\' + $env:USERNAME + '$'), "\$userName\$fileName"
if ((Test-Path $userPath) -or ($userDirectoryExists -and (-not $exitIfPathNotFound))) {
return $userPath
} }
} }
catch { catch {

View File

@@ -0,0 +1,80 @@
function Split-RegistryPath {
param(
[Parameter(Mandatory)]
[string]$path
)
$normalizedPath = [string]$path
if ([string]::IsNullOrWhiteSpace($normalizedPath)) {
return $null
}
$normalizedPath = $normalizedPath.Trim().Replace('/', '\')
if ([string]::IsNullOrWhiteSpace($normalizedPath)) {
return $null
}
if ($normalizedPath -notmatch '^(?<hive>HKEY_[^\\]+)(?:\\(?<subKey>.*))?$') {
return $null
}
$hiveName = [string]$matches.hive
$normalizedSubKey = if ($null -ne $matches.subKey) {
([string]$matches.subKey).Trim('\\')
}
else {
$null
}
if ($hiveName.Equals('HKEY_USERS', [System.StringComparison]::OrdinalIgnoreCase) -and -not [string]::IsNullOrWhiteSpace($normalizedSubKey)) {
if ($normalizedSubKey -match '^(?<mount>[^\\]+)(?:\\(?<rest>.*))?$') {
$mountName = [string]$matches.mount
if ($mountName.Equals('.DEFAULT', [System.StringComparison]::OrdinalIgnoreCase)) {
$remainingSubKey = if ($matches.rest) { [string]$matches.rest } else { '' }
if ([string]::IsNullOrWhiteSpace($remainingSubKey)) {
$normalizedSubKey = 'Default'
}
else {
$normalizedSubKey = "Default\$remainingSubKey"
}
}
}
}
return [PSCustomObject]@{
Hive = $hiveName
SubKey = $normalizedSubKey
}
}
function Get-RegistryRootKey {
param(
[Parameter(Mandatory)]
[string]$hiveName
)
switch ($hiveName.ToUpperInvariant()) {
'HKEY_CURRENT_USER' { return [Microsoft.Win32.Registry]::CurrentUser }
'HKEY_LOCAL_MACHINE' { return [Microsoft.Win32.Registry]::LocalMachine }
'HKEY_CLASSES_ROOT' { return [Microsoft.Win32.Registry]::ClassesRoot }
'HKEY_USERS' { return [Microsoft.Win32.Registry]::Users }
'HKEY_CURRENT_CONFIG' { return [Microsoft.Win32.Registry]::CurrentConfig }
default { return $null }
}
}
function Get-RegistryFilePathForFeature {
param(
[Parameter(Mandatory)]
$Feature,
[switch]$UseSysprepRegFiles
)
$useSysprepLayout = $UseSysprepRegFiles -or $script:Params.ContainsKey('Sysprep') -or $script:Params.ContainsKey('User')
if ($useSysprepLayout) {
return Join-Path (Join-Path $script:RegfilesPath 'Sysprep') $Feature.RegistryKey
}
return Join-Path $script:RegfilesPath $Feature.RegistryKey
}

View File

@@ -0,0 +1,382 @@
function NormalizeUserLookupValue {
param(
[string]$Value
)
if ([string]::IsNullOrWhiteSpace($Value)) {
return ''
}
# Remove zero-width characters and normalize whitespace for robust comparisons.
$normalized = $Value -replace '[\u200B-\u200D\uFEFF]', ''
$normalized = $normalized.Trim() -replace '\s+', ' '
return $normalized
}
if (-not $script:ResolvedUserSidCache) {
$script:ResolvedUserSidCache = @{}
}
function GetUserLookupCacheKey {
param(
[string]$Value
)
$normalizedValue = NormalizeUserLookupValue -Value $Value
if ([string]::IsNullOrWhiteSpace($normalizedValue)) {
return ''
}
return $normalizedValue.ToLowerInvariant()
}
function EscapeWqlString {
param(
[string]$Value
)
if ($null -eq $Value) {
return ''
}
return $Value -replace "'", "''"
}
function GetLocalUserNameSegment {
param(
[string]$UserName
)
$normalizedName = NormalizeUserLookupValue -Value $UserName
if ([string]::IsNullOrWhiteSpace($normalizedName)) {
return ''
}
if ($normalizedName.Contains('\')) {
return NormalizeUserLookupValue -Value (($normalizedName -split '\\')[-1])
}
if ($normalizedName.Contains('@')) {
return NormalizeUserLookupValue -Value (($normalizedName -split '@')[0])
}
return $normalizedName
}
function SetResolvedUserSidCache {
param(
[string[]]$Candidates,
[string]$Sid
)
if ([string]::IsNullOrWhiteSpace($Sid)) {
return
}
foreach ($candidate in @($Candidates)) {
$cacheKey = GetUserLookupCacheKey -Value $candidate
if ($cacheKey) {
$script:ResolvedUserSidCache[$cacheKey] = $Sid
}
}
}
function GetCachedResolvedUserSid {
param(
[string[]]$Candidates
)
foreach ($candidate in @($Candidates)) {
$cacheKey = GetUserLookupCacheKey -Value $candidate
if ($cacheKey -and $script:ResolvedUserSidCache.ContainsKey($cacheKey)) {
return $script:ResolvedUserSidCache[$cacheKey]
}
}
return $null
}
function TryResolveSidByNtAccount {
param(
[string]$UserName
)
if ([string]::IsNullOrWhiteSpace($UserName)) {
return $null
}
try {
$ntAccount = [System.Security.Principal.NTAccount]::new($UserName)
$sid = $ntAccount.Translate([System.Security.Principal.SecurityIdentifier])
if ($sid) {
return $sid.Value
}
}
catch {
# Fallback handled by caller.
}
return $null
}
function TryResolveSidByLocalLookup {
param(
[string[]]$Candidates
)
$lookupCandidates = @($Candidates) | ForEach-Object { NormalizeUserLookupValue -Value $_ } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique
if ($lookupCandidates.Count -eq 0) {
return $null
}
if (Get-Command -Name Get-LocalUser -ErrorAction SilentlyContinue) {
foreach ($candidate in $lookupCandidates) {
try {
$matchingLocalUser = Get-LocalUser -Name $candidate -ErrorAction Stop | Select-Object -First 1
if ($matchingLocalUser -and $matchingLocalUser.SID) {
return $matchingLocalUser.SID.Value
}
}
catch {
# Continue to next lookup strategy.
}
}
}
foreach ($candidate in $lookupCandidates) {
try {
$escapedCandidate = EscapeWqlString -Value $candidate
$escapedComputerName = EscapeWqlString -Value $env:COMPUTERNAME
$filter = "LocalAccount=True AND (Name='$escapedCandidate' OR FullName='$escapedCandidate' OR Caption='$escapedComputerName\$escapedCandidate')"
$matchingAccount = Get-CimInstance -ClassName Win32_UserAccount -Filter $filter -ErrorAction Stop | Select-Object -First 1
if ($matchingAccount -and $matchingAccount.SID) {
return $matchingAccount.SID
}
}
catch {
# Continue to next lookup strategy.
}
}
return $null
}
function TryResolveSidFromProfileList {
param(
[string[]]$Candidates
)
$lookupCandidates = @($Candidates) | ForEach-Object { NormalizeUserLookupValue -Value $_ } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique
if ($lookupCandidates.Count -eq 0) {
return $null
}
try {
$profileListPath = 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList'
foreach ($sidKey in @(Get-ChildItem -LiteralPath $profileListPath -ErrorAction Stop)) {
try {
$imagePath = Get-ItemPropertyValue -LiteralPath $sidKey.PSPath -Name 'ProfileImagePath' -ErrorAction Stop
if ([string]::IsNullOrWhiteSpace($imagePath)) { continue }
$expandedPath = [System.Environment]::ExpandEnvironmentVariables($imagePath)
$leafName = NormalizeUserLookupValue -Value (Split-Path -Leaf $expandedPath)
foreach ($candidate in $lookupCandidates) {
if ($leafName -ieq $candidate) {
return $sidKey.PSChildName
}
}
}
catch {
continue
}
}
}
catch {
# Fallback handled by caller.
}
return $null
}
function NewResolvedUserContext {
param(
[string]$UserName,
[string]$UserSid,
[string]$ProfilePath
)
return [PSCustomObject]@{
UserName = $UserName
UserSid = $UserSid
ProfilePath = $ProfilePath
}
}
function ResolveUserSid {
param(
[Parameter(Mandatory)]
[string]$UserName
)
$candidateUserName = NormalizeUserLookupValue -Value $UserName
if ([string]::IsNullOrWhiteSpace($candidateUserName)) {
return $null
}
$hasQualifiedIdentity = $candidateUserName.Contains('\') -or $candidateUserName.Contains('@')
$localNameSegment = GetLocalUserNameSegment -UserName $candidateUserName
$leafNameCandidates = @()
if ($hasQualifiedIdentity -and -not [string]::IsNullOrWhiteSpace($localNameSegment) -and $localNameSegment -ine $candidateUserName) {
$leafNameCandidates = @($localNameSegment)
}
$cacheCandidates = if ($hasQualifiedIdentity) {
@($candidateUserName)
}
else {
@($candidateUserName) + $leafNameCandidates | Select-Object -Unique
}
$localLookupCandidates = if ($hasQualifiedIdentity) {
@()
}
else {
@($candidateUserName) + $leafNameCandidates | Select-Object -Unique
}
$profileHeuristicCandidates = if ($leafNameCandidates.Count -gt 0) {
$leafNameCandidates
}
else {
@($candidateUserName)
}
$cachedSid = GetCachedResolvedUserSid -Candidates $cacheCandidates
if ($cachedSid) {
return $cachedSid
}
# Resolve fully-qualified identities first to avoid accidentally matching a local leaf account.
if ($hasQualifiedIdentity) {
$resolvedSid = TryResolveSidByNtAccount -UserName $candidateUserName
if ($resolvedSid) {
SetResolvedUserSidCache -Candidates $cacheCandidates -Sid $resolvedSid
return $resolvedSid
}
}
$resolvedSid = TryResolveSidByLocalLookup -Candidates $localLookupCandidates
if ($resolvedSid) {
SetResolvedUserSidCache -Candidates $cacheCandidates -Sid $resolvedSid
return $resolvedSid
}
# Last-ditch NTAccount translation for non-qualified names.
if (-not $hasQualifiedIdentity) {
$resolvedSid = TryResolveSidByNtAccount -UserName $candidateUserName
if ($resolvedSid) {
SetResolvedUserSidCache -Candidates $cacheCandidates -Sid $resolvedSid
return $resolvedSid
}
}
$resolvedSid = TryResolveSidFromProfileList -Candidates $profileHeuristicCandidates
if ($resolvedSid) {
SetResolvedUserSidCache -Candidates $cacheCandidates -Sid $resolvedSid
return $resolvedSid
}
return $null
}
function ResolveUserProfilePath {
param(
[Parameter(Mandatory)]
[string]$UserName
)
$userContext = ResolveUserProfileContext -UserName $UserName
if ($userContext) {
return $userContext.ProfilePath
}
return $null
}
function ResolveUserProfileContext {
param(
[Parameter(Mandatory)]
[string]$UserName
)
if ([string]::IsNullOrWhiteSpace($UserName)) {
return $null
}
$candidateUserName = NormalizeUserLookupValue -Value $UserName
$rootPaths = @(
(Join-Path $env:SystemDrive 'Users')
(Split-Path -Path $env:USERPROFILE -Parent)
) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique
if ($candidateUserName -ieq 'Default') {
foreach ($rootPath in $rootPaths) {
if (-not (Test-Path -LiteralPath $rootPath -PathType Container)) {
continue
}
$defaultProfilePath = Join-Path $rootPath 'Default'
if (Test-Path -LiteralPath $defaultProfilePath -PathType Container) {
return (NewResolvedUserContext -UserName $candidateUserName -UserSid $null -ProfilePath $defaultProfilePath)
}
}
return $null
}
$userSid = ResolveUserSid -UserName $candidateUserName
if ($userSid) {
$sidRegistryPath = "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\$userSid"
try {
if (Test-Path -LiteralPath $sidRegistryPath) {
$registryImagePath = Get-ItemPropertyValue -LiteralPath $sidRegistryPath -Name 'ProfileImagePath' -ErrorAction Stop
if (-not [string]::IsNullOrWhiteSpace($registryImagePath)) {
$expandedPath = [System.Environment]::ExpandEnvironmentVariables($registryImagePath)
if (Test-Path -LiteralPath $expandedPath -PathType Container) {
return (NewResolvedUserContext -UserName $candidateUserName -UserSid $userSid -ProfilePath $expandedPath)
}
}
}
}
catch {
# Try Win32_UserProfile fallback.
}
try {
$matchingProfiles = @(Get-CimInstance -ClassName Win32_UserProfile -Filter "SID='$userSid'" -ErrorAction Stop)
$resolvedProfile = $matchingProfiles | Where-Object { -not [string]::IsNullOrWhiteSpace($_.LocalPath) } | Select-Object -First 1
if ($resolvedProfile -and (Test-Path -LiteralPath $resolvedProfile.LocalPath -PathType Container)) {
return (NewResolvedUserContext -UserName $candidateUserName -UserSid $userSid -ProfilePath $resolvedProfile.LocalPath)
}
}
catch {
# Fall through to legacy path probing.
}
}
foreach ($rootPath in $rootPaths) {
if (-not (Test-Path -LiteralPath $rootPath -PathType Container)) {
continue
}
$candidateUserPath = Join-Path $rootPath $candidateUserName
if (Test-Path -LiteralPath $candidateUserPath -PathType Container) {
return (NewResolvedUserContext -UserName $candidateUserName -UserSid $userSid -ProfilePath $candidateUserPath)
}
}
return $null
}

View File

@@ -0,0 +1,47 @@
function Test-TargetUserName {
param(
[AllowNull()]
[AllowEmptyString()]
[string]$UserName
)
$normalizedUserName = if ($null -ne $UserName) { $UserName.Trim() } else { '' }
if ([string]::IsNullOrWhiteSpace($normalizedUserName)) {
return [PSCustomObject]@{
IsValid = $false
UserName = $normalizedUserName
Message = 'Please enter a username'
}
}
if ($normalizedUserName -eq $env:USERNAME) {
return [PSCustomObject]@{
IsValid = $false
UserName = $normalizedUserName
Message = "Cannot enter your own username, use 'Current User' option instead"
}
}
if (-not (CheckIfUserExists -userName $normalizedUserName)) {
return [PSCustomObject]@{
IsValid = $false
UserName = $normalizedUserName
Message = 'User not found, please enter a valid username'
}
}
if (TestIfUserIsLoggedIn -Username $normalizedUserName) {
return [PSCustomObject]@{
IsValid = $false
UserName = $normalizedUserName
Message = "User '$normalizedUserName' is currently logged in. Please sign out that user first."
}
}
return [PSCustomObject]@{
IsValid = $true
UserName = $normalizedUserName
Message = "User found: $normalizedUserName"
}
}

View File

@@ -141,24 +141,31 @@ if (-not $isAdmin) {
} }
# Define script-level variables & paths # Define script-level variables & paths
$script:Version = "2026.04.26" $script:Version = "2026.05.11"
$script:AppsListFilePath = "$PSScriptRoot/Config/Apps.json" $configPath = Join-Path $PSScriptRoot 'Config'
$script:DefaultSettingsFilePath = "$PSScriptRoot/Config/DefaultSettings.json" $logsPath = Join-Path $PSScriptRoot 'Logs'
$script:FeaturesFilePath = "$PSScriptRoot/Config/Features.json" $schemasPath = Join-Path $PSScriptRoot 'Schemas'
$script:SavedSettingsFilePath = "$PSScriptRoot/Config/LastUsedSettings.json" $scriptsPath = Join-Path $PSScriptRoot 'Scripts'
$script:CustomAppsListFilePath = "$PSScriptRoot/Config/CustomAppsList"
$script:DefaultLogPath = "$PSScriptRoot/Logs/Win11Debloat.log" $script:AppsListFilePath = Join-Path $configPath 'Apps.json'
$script:RegfilesPath = "$PSScriptRoot/Regfiles" $script:DefaultSettingsFilePath = Join-Path $configPath 'DefaultSettings.json'
$script:AssetsPath = "$PSScriptRoot/Assets" $script:FeaturesFilePath = Join-Path $configPath 'Features.json'
$script:AppSelectionSchema = "$PSScriptRoot/Schemas/AppSelectionWindow.xaml" $script:SavedSettingsFilePath = Join-Path $configPath 'LastUsedSettings.json'
$script:MainWindowSchema = "$PSScriptRoot/Schemas/MainWindow.xaml" $script:CustomAppsListFilePath = Join-Path $configPath 'CustomAppsList'
$script:MessageBoxSchema = "$PSScriptRoot/Schemas/MessageBoxWindow.xaml" $script:DefaultLogPath = Join-Path $logsPath 'Win11Debloat.log'
$script:AboutWindowSchema = "$PSScriptRoot/Schemas/AboutWindow.xaml" $script:RegfilesPath = Join-Path $PSScriptRoot 'Regfiles'
$script:ApplyChangesWindowSchema = "$PSScriptRoot/Schemas/ApplyChangesWindow.xaml" $script:RegistryBackupsPath = Join-Path $PSScriptRoot 'Backups'
$script:SharedStylesSchema = "$PSScriptRoot/Schemas/SharedStyles.xaml" $script:AssetsPath = Join-Path $PSScriptRoot 'Assets'
$script:BubbleHintSchema = "$PSScriptRoot/Schemas/BubbleHint.xaml" $script:AppSelectionSchema = Join-Path $schemasPath 'AppSelectionWindow.xaml'
$script:ImportExportConfigSchema = "$PSScriptRoot/Schemas/ImportExportConfigWindow.xaml" $script:MainWindowSchema = Join-Path $schemasPath 'MainWindow.xaml'
$script:LoadAppsDetailsScriptPath = "$PSScriptRoot/Scripts/FileIO/LoadAppsDetailsFromJson.ps1" $script:MessageBoxSchema = Join-Path $schemasPath 'MessageBoxWindow.xaml'
$script:AboutWindowSchema = Join-Path $schemasPath 'AboutWindow.xaml'
$script:ApplyChangesWindowSchema = Join-Path $schemasPath 'ApplyChangesWindow.xaml'
$script:SharedStylesSchema = Join-Path $schemasPath 'SharedStyles.xaml'
$script:BubbleHintSchema = Join-Path $schemasPath 'BubbleHint.xaml'
$script:ImportExportConfigSchema = Join-Path $schemasPath 'ImportExportConfigWindow.xaml'
$script:RestoreBackupWindowSchema = Join-Path $schemasPath 'RestoreBackupWindow.xaml'
$script:LoadAppsDetailsScriptPath = Join-Path (Join-Path $scriptsPath 'FileIO') 'LoadAppsDetailsFromJson.ps1'
$script:ControlParams = 'WhatIf', 'Confirm', 'Verbose', 'Debug', 'LogPath', 'Silent', 'Sysprep', 'User', 'NoRestartExplorer', 'RunDefaults', 'RunDefaultsLite', 'RunSavedSettings', 'Config', 'RunAppsListGenerator', 'CLI', 'AppRemovalTarget' $script:ControlParams = 'WhatIf', 'Confirm', 'Verbose', 'Debug', 'LogPath', 'Silent', 'Sysprep', 'User', 'NoRestartExplorer', 'RunDefaults', 'RunDefaultsLite', 'RunSavedSettings', 'Config', 'RunAppsListGenerator', 'CLI', 'AppRemovalTarget'
@@ -209,14 +216,14 @@ Write-Host ""
# Log script output to 'Win11Debloat.log' at the specified path # Log script output to 'Win11Debloat.log' at the specified path
if ($LogPath -and (Test-Path $LogPath)) { if ($LogPath -and (Test-Path $LogPath)) {
Start-Transcript -Path "$LogPath/Win11Debloat.log" -Append -IncludeInvocationHeader -Force | Out-Null Start-Transcript -Path (Join-Path $LogPath 'Win11Debloat.log') -Append -IncludeInvocationHeader -Force | Out-Null
} }
else { else {
Start-Transcript -Path $script:DefaultLogPath -Append -IncludeInvocationHeader -Force | Out-Null Start-Transcript -Path $script:DefaultLogPath -Append -IncludeInvocationHeader -Force | Out-Null
} }
# Check if script has all required files # Check if script has all required files
if (-not ((Test-Path $script:DefaultSettingsFilePath) -and (Test-Path $script:AppsListFilePath) -and (Test-Path $script:RegfilesPath) -and (Test-Path $script:AssetsPath) -and (Test-Path $script:AppSelectionSchema) -and (Test-Path $script:ApplyChangesWindowSchema) -and (Test-Path $script:SharedStylesSchema) -and (Test-Path $script:BubbleHintSchema) -and (Test-Path $script:FeaturesFilePath))) { if (-not ((Test-Path $script:DefaultSettingsFilePath) -and (Test-Path $script:AppsListFilePath) -and (Test-Path $script:RegfilesPath) -and (Test-Path $script:AssetsPath) -and (Test-Path $script:AppSelectionSchema) -and (Test-Path $script:ApplyChangesWindowSchema) -and (Test-Path $script:SharedStylesSchema) -and (Test-Path $script:BubbleHintSchema) -and (Test-Path $script:RestoreBackupWindowSchema) -and (Test-Path $script:FeaturesFilePath))) {
Write-Error "Win11Debloat is unable to find required files, please ensure all script files are present" Write-Error "Win11Debloat is unable to find required files, please ensure all script files are present"
Write-Output "" Write-Output ""
Write-Output "Press any key to exit..." Write-Output "Press any key to exit..."
@@ -288,6 +295,12 @@ if (-not $script:WingetInstalled -and -not $Silent) {
# Features functions # Features functions
. "$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/BackupRegistrySnapshotCapture.ps1"
. "$PSScriptRoot/Scripts/Features/BackupRegistryState.ps1"
. "$PSScriptRoot/Scripts/Features/RegistryBackupValidation.ps1"
. "$PSScriptRoot/Scripts/Features/RestoreRegistryApplyState.ps1"
. "$PSScriptRoot/Scripts/Features/RestoreRegistryBackup.ps1"
. "$PSScriptRoot/Scripts/Features/DisableStoreSearchSuggestions.ps1" . "$PSScriptRoot/Scripts/Features/DisableStoreSearchSuggestions.ps1"
. "$PSScriptRoot/Scripts/Features/EnableWindowsFeature.ps1" . "$PSScriptRoot/Scripts/Features/EnableWindowsFeature.ps1"
. "$PSScriptRoot/Scripts/Features/ImportRegistryFile.ps1" . "$PSScriptRoot/Scripts/Features/ImportRegistryFile.ps1"
@@ -314,20 +327,29 @@ if (-not $script:WingetInstalled -and -not $Silent) {
. "$PSScriptRoot/Scripts/GUI/Show-ConfigWindow.ps1" . "$PSScriptRoot/Scripts/GUI/Show-ConfigWindow.ps1"
. "$PSScriptRoot/Scripts/GUI/Show-ApplyModal.ps1" . "$PSScriptRoot/Scripts/GUI/Show-ApplyModal.ps1"
. "$PSScriptRoot/Scripts/GUI/Show-AppSelectionWindow.ps1" . "$PSScriptRoot/Scripts/GUI/Show-AppSelectionWindow.ps1"
. "$PSScriptRoot/Scripts/GUI/Show-RestoreBackupWindow.ps1"
. "$PSScriptRoot/Scripts/GUI/RestoreBackupDialogFeatureLists.ps1"
. "$PSScriptRoot/Scripts/GUI/Show-RestoreBackupDialog.ps1"
. "$PSScriptRoot/Scripts/GUI/Show-MainWindow.ps1" . "$PSScriptRoot/Scripts/GUI/Show-MainWindow.ps1"
. "$PSScriptRoot/Scripts/GUI/Show-AboutDialog.ps1" . "$PSScriptRoot/Scripts/GUI/Show-AboutDialog.ps1"
. "$PSScriptRoot/Scripts/GUI/Show-Bubble.ps1" . "$PSScriptRoot/Scripts/GUI/Show-Bubble.ps1"
# Helper functions # Helper functions
. "$PSScriptRoot/Scripts/Helpers/AddParameter.ps1" . "$PSScriptRoot/Scripts/Helpers/AddParameter.ps1"
. "$PSScriptRoot/Scripts/Helpers/ResolveUserProfilePath.ps1"
. "$PSScriptRoot/Scripts/Helpers/CheckIfUserExists.ps1" . "$PSScriptRoot/Scripts/Helpers/CheckIfUserExists.ps1"
. "$PSScriptRoot/Scripts/Helpers/CheckModernStandbySupport.ps1" . "$PSScriptRoot/Scripts/Helpers/CheckModernStandbySupport.ps1"
. "$PSScriptRoot/Scripts/Helpers/GenerateAppsList.ps1" . "$PSScriptRoot/Scripts/Helpers/GenerateAppsList.ps1"
. "$PSScriptRoot/Scripts/Helpers/GetFriendlyRegistryBackupTarget.ps1"
. "$PSScriptRoot/Scripts/Helpers/GetFriendlyTargetUserName.ps1" . "$PSScriptRoot/Scripts/Helpers/GetFriendlyTargetUserName.ps1"
. "$PSScriptRoot/Scripts/Helpers/ImportConfigToParams.ps1" . "$PSScriptRoot/Scripts/Helpers/ImportConfigToParams.ps1"
. "$PSScriptRoot/Scripts/Helpers/GetTargetUserForAppRemoval.ps1" . "$PSScriptRoot/Scripts/Helpers/GetTargetUserForAppRemoval.ps1"
. "$PSScriptRoot/Scripts/Helpers/Get-RegFileOperations.ps1"
. "$PSScriptRoot/Scripts/Helpers/Test-TargetUserName.ps1"
. "$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/ApplyRegistryRegFile.ps1"
. "$PSScriptRoot/Scripts/Helpers/TestIfUserIsLoggedIn.ps1" . "$PSScriptRoot/Scripts/Helpers/TestIfUserIsLoggedIn.ps1"
# Threading functions # Threading functions
@@ -380,7 +402,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) {
@@ -391,10 +413,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