mirror of
https://github.com/Raphire/Win11Debloat.git
synced 2026-08-23 08:02:07 +00:00
Compare commits
19
Commits
2026.06.24
...
2026.07.11
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce36b8edfb | ||
|
|
3b78e8d4ed | ||
|
|
74dedc00e9 | ||
|
|
5571700634 | ||
|
|
c2ea697552 | ||
|
|
ce8c10042e | ||
|
|
89a5d508d8 | ||
|
|
e38aafe01e | ||
|
|
d336f98b39 | ||
|
|
a70a2ccbd6 | ||
|
|
0ef789cc3a | ||
|
|
82f50d6d13 | ||
|
|
949515da3e | ||
|
|
0eeb8f323a | ||
|
|
a9a89c1f13 | ||
|
|
339764eb18 | ||
|
|
3b4a5f16ad | ||
|
|
9a700afaed | ||
|
|
26b313372b |
+18
-2
@@ -18,7 +18,7 @@ You can help us test the latest changes and additions to the script. If you enco
|
||||
You can launch the prerelease version of Win11Debloat by running this command:
|
||||
|
||||
```ps1
|
||||
& ([scriptblock]::Create((irm "https://debloat.raphi.re/dev")))
|
||||
& ([scriptblock]::Create((irm "https://debloat.raphi.re/"))) -Dev
|
||||
```
|
||||
|
||||
# Contributing Code
|
||||
@@ -159,10 +159,26 @@ To add a new app that can be removed via Win11Debloat:
|
||||
"FriendlyName": "Display Name",
|
||||
"AppId": "AppPackageIdentifier",
|
||||
"Description": "Brief description of the app",
|
||||
"SelectedByDefault": true|false
|
||||
"SelectedByDefault": false,
|
||||
"Recommendation": "optional",
|
||||
"RemovalMethod": "Appx"
|
||||
}
|
||||
```
|
||||
|
||||
**Field Descriptions**:
|
||||
|
||||
- `FriendlyName`: Display name shown in the GUI.
|
||||
- `AppId`: The `AppPackageIdentifier` from `Get-AppxPackage` or the `Id` from `winget list`, depending on removal method.
|
||||
- `Description`: Brief description of the app shown in the GUI.
|
||||
- `SelectedByDefault`: Set to `true` only for apps that are largely considered bloatware, otherwise set to `false`.
|
||||
- `Recommendation`: Indicates how strongly the app is recommended for removal. One of:
|
||||
- `safe` — safe to remove for most users
|
||||
- `optional` — can be safely removed if the user doesn't need the app
|
||||
- `unsafe` — should only remove if the user knows what they are doing
|
||||
- `RemovalMethod`: The method used to remove the app. One of:
|
||||
- `Appx` — remove as a standard Appx package via `Remove-AppxPackage` (most apps)
|
||||
- `WinGet` — remove via WinGet (`winget uninstall`). Use for non-Appx apps such as Microsoft Copilot.
|
||||
|
||||
3. **Follow the Guidelines**:
|
||||
|
||||
- Use clear, user-friendly names for `FriendlyName`
|
||||
|
||||
+10
-1
@@ -921,6 +921,14 @@
|
||||
"Recommendation": "optional",
|
||||
"RemovalMethod": "Appx"
|
||||
},
|
||||
{
|
||||
"FriendlyName": "LG Monitor App",
|
||||
"AppId": "LGElectronics.LGMonitorApp",
|
||||
"Description": "LG OEM software for controlling your LG monitor, Windows Update may silently reinstall this app if you have an LG monitor connected",
|
||||
"SelectedByDefault": false,
|
||||
"Recommendation": "optional",
|
||||
"RemovalMethod": "Appx"
|
||||
},
|
||||
{
|
||||
"FriendlyName": "HP AI Experience Center",
|
||||
"AppId": "AD2F1837.HPAIExperienceCenter",
|
||||
@@ -1132,7 +1140,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "OEM software (Dell, HP, Lenovo)",
|
||||
"Name": "OEM software (Dell, HP, Lenovo, LG)",
|
||||
"AppIds": [
|
||||
"AD2F1837.HPAIExperienceCenter",
|
||||
"AD2F1837.HPConnectedMusic",
|
||||
@@ -1156,6 +1164,7 @@
|
||||
"AD2F1837.myHP",
|
||||
"E046963F.LenovoCompanion",
|
||||
"LenovoCompanyLimited.LenovoVantageService",
|
||||
"LGElectronics.LGMonitorApp",
|
||||
"DellInc.DellSupportAssistforPCs",
|
||||
"DellInc.DellDigitalDelivery",
|
||||
"DellInc.DellMobileConnect"
|
||||
|
||||
+29
-2
@@ -401,6 +401,19 @@
|
||||
"MinVersion": null,
|
||||
"MaxVersion": null
|
||||
},
|
||||
{
|
||||
"FeatureId": "DisableNotifications",
|
||||
"Label": "Disable Windows Notifications (From apps and other senders)",
|
||||
"ToolTip": "Disables native Windows notifications from apps and other senders, this includes notifications from apps like Discord, WhatsApp, Teams, and Slack.",
|
||||
"Category": "Privacy & Suggested Content",
|
||||
"RegistryKey": "Disable_Notifications.reg",
|
||||
"ApplyText": "Disabling Windows notifications and reminders",
|
||||
"UndoLabel": "Enable Windows notifications and reminders",
|
||||
"ApplyUndoText": "Enabling Windows notifications and reminders",
|
||||
"RegistryUndoKey": "Enable_Notifications.reg",
|
||||
"MinVersion": null,
|
||||
"MaxVersion": null
|
||||
},
|
||||
{
|
||||
"FeatureId": "DisableLocationServices",
|
||||
"Label": "Disable Windows location services & app location access",
|
||||
@@ -667,7 +680,7 @@
|
||||
"UndoLabel": "Show Microsoft 365 Copilot ads in Settings Home",
|
||||
"ApplyUndoText": "Enabling Microsoft 365 Copilot ads in Settings Home",
|
||||
"RegistryUndoKey": "Enable_Settings_365_Ads.reg",
|
||||
"MinVersion": null,
|
||||
"MinVersion": 22000,
|
||||
"MaxVersion": null
|
||||
},
|
||||
{
|
||||
@@ -680,7 +693,7 @@
|
||||
"UndoLabel": "Show Settings 'Home' page",
|
||||
"ApplyUndoText": "Enabling the Settings Home page",
|
||||
"RegistryUndoKey": "Enable_Settings_Home.reg",
|
||||
"MinVersion": null,
|
||||
"MinVersion": 22000,
|
||||
"MaxVersion": null
|
||||
},
|
||||
{
|
||||
@@ -800,6 +813,7 @@
|
||||
"UndoLabel": "Enable window snapping",
|
||||
"ApplyUndoText": "Enabling window snapping",
|
||||
"RegistryUndoKey": "Enable_Window_Snapping.reg",
|
||||
"RequiresReboot": true,
|
||||
"MinVersion": null,
|
||||
"MaxVersion": null
|
||||
},
|
||||
@@ -1356,6 +1370,19 @@
|
||||
"MinVersion": null,
|
||||
"MaxVersion": null
|
||||
},
|
||||
{
|
||||
"FeatureId": "DisableDeviceAutoAppDownload",
|
||||
"Label": "Prevent Windows from auto-installing device companion apps",
|
||||
"ToolTip": "Stops Windows from silently installing device companion apps via Windows Update when you connect certain devices, like monitors from LG and Alienware. These devices will still work as expected, this only stops Windows Update from automatically installing additional apps without consent.",
|
||||
"Category": "Windows Update",
|
||||
"RegistryKey": "Disable_Device_Auto_App_Download.reg",
|
||||
"ApplyText": "Preventing automatic installation of device companion apps",
|
||||
"UndoLabel": "Allow automatic installation of device companion apps",
|
||||
"ApplyUndoText": "Allowing automatic installation of device companion apps",
|
||||
"RegistryUndoKey": "Enable_Device_Auto_App_Download.reg",
|
||||
"MinVersion": null,
|
||||
"MaxVersion": null
|
||||
},
|
||||
{
|
||||
"FeatureId": "ForceRemoveEdge",
|
||||
"Label": "Forcefully uninstall Microsoft Edge. NOT RECOMMENDED!",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
Win11Debloat is a lightweight, easy to use PowerShell script that allows you to quickly declutter and customize your Windows experience, no installation required! You can use it to remove pre-installed apps, disable telemetry, remove intrusive interface elements and much more. No need to painstakingly go through all the settings yourself or remove apps one by one. Win11Debloat makes the process quick and easy!
|
||||
|
||||
The script also includes many features that system administrators and power users will enjoy. Such as a powerful command-line interface, support for Windows Audit mode and the ability to make changes to other Windows users. Please refer to our [wiki](https://github.com/Raphire/Win11Debloat/wiki/) for more details.
|
||||
The script also includes many features that system administrators and power users will enjoy. Such as a powerful command-line interface, support for Windows Audit mode and the ability to make changes to other Windows users. You can also easily export & import your preferred settings, allowing you to quickly apply the same settings on all your systems. Please refer to our [wiki](https://github.com/Raphire/Win11Debloat/wiki) for more details.
|
||||
|
||||

|
||||
|
||||
@@ -23,14 +23,14 @@ The script also includes many features that system administrators and power user
|
||||
|
||||
Download & run the script automatically via PowerShell.
|
||||
|
||||
1. Open PowerShell or Terminal, preferably as an administrator.
|
||||
1. Open PowerShell or Terminal.
|
||||
2. Copy and paste the command below into PowerShell:
|
||||
|
||||
```PowerShell
|
||||
& ([scriptblock]::Create((irm "https://debloat.raphi.re/")))
|
||||
```
|
||||
|
||||
3. Wait for the script to automatically download Win11Debloat.
|
||||
3. Wait for the script to automatically download and launch Win11Debloat.
|
||||
4. Carefully read through and follow the on-screen instructions.
|
||||
|
||||
This method supports command-line parameters to customize the behaviour of the script. Please click [here](https://github.com/Raphire/Win11Debloat/wiki/Command%E2%80%90line-Interface#parameters) for more information.
|
||||
@@ -74,10 +74,10 @@ This method supports command-line parameters to customize the behaviour of the s
|
||||
|
||||
## Features
|
||||
|
||||
Below is an overview of the key features and functionality offered by Win11Debloat. Please refer to [the wiki](https://github.com/Raphire/Win11Debloat/wiki/Default-Settings) for more information about the default settings preset.
|
||||
Below is an overview of the key features and functionality offered by Win11Debloat. You can visit the [the wiki](https://github.com/Raphire/Win11Debloat/wiki) for more details.
|
||||
|
||||
> [!Tip]
|
||||
> All of the changes made by Win11Debloat can easily be reverted and almost all of the apps can be reinstalled through the Microsoft Store. A full guide on how to revert changes can be found [here](https://github.com/Raphire/Win11Debloat/wiki/Reverting-Changes).
|
||||
> All of the changes made by Win11Debloat can easily be reverted and almost all of the apps can be reinstalled through the Microsoft Store. You can visit [the wiki](https://github.com/Raphire/Win11Debloat/wiki/Reverting-Changes) for more information on reverting changes.
|
||||
|
||||
#### App Removal
|
||||
|
||||
@@ -86,29 +86,21 @@ Below is an overview of the key features and functionality offered by Win11Deblo
|
||||
#### Privacy & Suggested Content
|
||||
|
||||
- Disable telemetry, diagnostic data, activity history, app-launch tracking & targeted ads.
|
||||
- Disable tips, tricks, suggestions & ads across Windows.
|
||||
- Disable Windows location services & app location access.
|
||||
- Disable Find My Device location tracking.
|
||||
- Disable 'Windows Spotlight' and tips & tricks on the lock screen.
|
||||
- Disable 'Windows Spotlight' desktop background option.
|
||||
- Disable ads, suggestions and the MSN news feed in Microsoft Edge.
|
||||
- Disable tips, tricks, suggestions & ads across Windows, the lock screen and Microsoft Edge.
|
||||
- Disable Windows location services, app location access and Find My Device location tracking.
|
||||
- Hide Microsoft 365 ads on the Settings 'Home' page, or hide the 'Home' page entirely.
|
||||
|
||||
#### AI Features
|
||||
|
||||
- Disable & remove Microsoft Copilot.
|
||||
- Disable Windows Recall.
|
||||
- Disable Click to Do, AI text & image analysis tool.
|
||||
- Disable & remove Microsoft Copilot, Windows Recall and Click to Do.
|
||||
- Prevent AI service (WSAIFabricSvc) from starting automatically.
|
||||
- Disable AI Features in Edge.
|
||||
- Disable AI Features in Paint.
|
||||
- Disable AI Features in Notepad.
|
||||
- Disable AI Features in Edge, Paint and Notepad.
|
||||
|
||||
#### System
|
||||
|
||||
- Disable the Drag Tray for sharing & moving files.
|
||||
- Restore the old Windows 10 style context menu.
|
||||
- Turn off Enhance Pointer Precision, also known as mouse acceleration.
|
||||
- Turn off Enhance Pointer Precision (mouse acceleration).
|
||||
- Disable the Sticky Keys keyboard shortcut.
|
||||
- Disable Storage Sense automatic disk cleanup.
|
||||
- Disable fast start-up to ensure a full shutdown.
|
||||
@@ -120,54 +112,43 @@ Below is an overview of the key features and functionality offered by Win11Deblo
|
||||
- Prevent Windows from getting updates as soon as they're available.
|
||||
- Prevent automatic restarts after updates while signed in.
|
||||
- Disable sharing of downloaded updates with other PCs, also known as Delivery Optimization.
|
||||
- Prevent Windows from auto-installing device companion apps.
|
||||
|
||||
#### Appearance
|
||||
|
||||
- Enable dark mode for system and apps.
|
||||
- Disable transparency effects
|
||||
- Disable animations and visual effects.
|
||||
- Disable transparency, animations and visual effects.
|
||||
|
||||
#### Start Menu & Search
|
||||
|
||||
- Remove or replace all pinned apps from the start menu.
|
||||
- Hide the recommended section in the start menu.
|
||||
- Hide the 'All Apps' section in the start menu.
|
||||
- Customize the start menu by removing pinned apps, hiding recommendations, and customizing the 'All Apps' section.
|
||||
- Disable the Phone Link mobile devices integration in the start menu.
|
||||
- Disable Bing web search & Copilot integration in Windows search.
|
||||
- Disable Microsoft Store app suggestions in Windows search.
|
||||
- Disable Search Highlights (dynamic/branded content) in the taskbar search box.
|
||||
- Disable local Windows search history.
|
||||
- Disable Bing web search & Copilot integration and Microsoft Store app suggestions in Windows search.
|
||||
|
||||
#### Taskbar
|
||||
|
||||
- Align taskbar icons to the left.
|
||||
- Hide or change the search icon/box on the taskbar.
|
||||
- Hide the taskview button from the taskbar.
|
||||
- Change taskbar alignment.
|
||||
- Customize or hide taskbar buttons like the search bar, taskview and more.
|
||||
- Disable widgets on the taskbar & lock screen.
|
||||
- Hide the chat (meet now) icon from the taskbar.
|
||||
- Enable the 'End Task' option in the taskbar right click menu.
|
||||
- Enable the 'End Task' option in the taskbar right click menu to quickly force-close apps.
|
||||
- Enable the 'Last Active Click' behavior in the taskbar app area. This allows you to repeatedly click on an application's icon in the taskbar to switch focus between the open windows of that application.
|
||||
- Choose how app icons are shown on the taskbar when using multiple monitors.
|
||||
- Choose combine mode for taskbar buttons and labels.
|
||||
- Customize how app buttons are shown on the taskbar.
|
||||
|
||||
#### File Explorer
|
||||
|
||||
- Change the default location that File Explorer opens to.
|
||||
- Show file extensions for known file types.
|
||||
- Show hidden files, folders and drives.
|
||||
- Hide the Home or Gallery section from the File Explorer navigation pane.
|
||||
- Hide the Home, Gallery or OneDrive section from the File Explorer navigation pane.
|
||||
- Hide duplicate removable drive entries from the File Explorer navigation pane, so only the entry under 'This PC' remains.
|
||||
- Add all common folders (Desktop, Downloads, etc.) back to 'This PC' in File Explorer.
|
||||
- Hide the 3D objects, music or OneDrive folder from the File Explorer navigation pane.
|
||||
- Hide the 'Include in library', 'Give access to' and 'Share' options from the context menu.
|
||||
- Change drive letter position or visibility in File Explorer.
|
||||
|
||||
#### Multi-tasking
|
||||
|
||||
- Disable window snapping.
|
||||
- Disable Snap Assist suggestions when snapping a window.
|
||||
- Disable Snap Layout suggestions when dragging windows to the top of screen and when hovering on the maximize button.
|
||||
- Change if tabs are shown when snapping or pressing Alt+Tab.
|
||||
- Disable Snap Assist and Snap Layout suggestions when dragging or snapping windows.
|
||||
- Change whether tabs are shown when snapping windows or pressing Alt+Tab.
|
||||
|
||||
#### Optional Windows Features
|
||||
|
||||
@@ -181,12 +162,12 @@ Below is an overview of the key features and functionality offered by Win11Deblo
|
||||
|
||||
#### Advanced Features
|
||||
|
||||
- Option to [apply changes to a different user](https://github.com/Raphire/Win11Debloat/wiki/Advanced-Features#running-as-another-user), instead of the currently logged in user.
|
||||
- Ability to [apply changes to a different user](https://github.com/Raphire/Win11Debloat/wiki/Advanced-Features#running-as-another-user), instead of the currently logged in user.
|
||||
- [Sysprep mode](https://github.com/Raphire/Win11Debloat/wiki/Advanced-Features#sysprep-mode) to apply changes to the Windows Default user profile. Which ensures, all new users will have the changes automatically applied to them.
|
||||
|
||||
## Contributing
|
||||
|
||||
We welcome contributions of all kinds! Please see our [Contributing Guidelines](/.github/CONTRIBUTING.md) for detailed instructions on how to get started and best practices for contributing.
|
||||
We welcome contributions of all kinds! Please see our [Contributing Guidelines](https://github.com/Raphire/Win11Debloat/blob/master/.github/CONTRIBUTING.md) for detailed instructions on how to get started and best practices for contributing.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,5 +1,5 @@
|
||||
@echo off
|
||||
setlocal EnableDelayedExpansion
|
||||
setlocal
|
||||
|
||||
:: Set Windows Terminal installation paths. (Default and Scoop installation)
|
||||
set "wtDefaultPath=%LOCALAPPDATA%\Microsoft\WindowsApps\wt.exe"
|
||||
@@ -15,22 +15,20 @@ if exist "%wtDefaultPath%" (
|
||||
) else if exist "%wtScoopPath%" (
|
||||
set "wtPath=%wtScoopPath%"
|
||||
) else (
|
||||
echo Windows Terminal not found. Using default PowerShell instead.
|
||||
set "wtPath="
|
||||
)
|
||||
|
||||
set "SCRIPT_PATH=\"%~dp0Win11Debloat.ps1\""
|
||||
:: Interpolated into a PS single-quoted string below;
|
||||
:: Apostrophes escaped via %:'=''% and -File arg uses [char]34 to avoid quote-parity bugs.
|
||||
set "SCRIPT_PATH=%~dp0Win11Debloat.ps1"
|
||||
|
||||
:: Launch script
|
||||
if defined wtPath (
|
||||
call :Log Launching Win11Debloat.ps1 with Windows Terminal...
|
||||
PowerShell -Command "Start-Process -FilePath '%wtPath%' -ArgumentList 'PowerShell -NoProfile -ExecutionPolicy Bypass -File %SCRIPT_PATH%' -Verb RunAs" >> "%logFile%" || call :Error "PowerShell command failed"
|
||||
call :Log Script execution passed successfully to Win11Debloat.ps1
|
||||
PowerShell -NoProfile -ExecutionPolicy Bypass -Command "$p='%SCRIPT_PATH:'=''%'; $q=[char]34; Start-Process -FilePath '%wtPath%' -ArgumentList ('PowerShell -NoProfile -ExecutionPolicy Bypass -File ' + $q + $p + $q) -Verb RunAs" >> "%logFile%" || call :Error "PowerShell command failed"
|
||||
) else (
|
||||
echo Windows Terminal not found. Using default PowerShell instead...
|
||||
echo Windows Terminal not found, using default PowerShell...
|
||||
call :Log Windows Terminal not found. Using default PowerShell to launch Win11Debloat.ps1...
|
||||
PowerShell -ExecutionPolicy Bypass -Command "& {Start-Process PowerShell -ArgumentList '-NoProfile -ExecutionPolicy Bypass -File %SCRIPT_PATH%' -Verb RunAs}" >> "%logFile%" || call :Error "PowerShell command failed"
|
||||
call :Log Script execution passed successfully to Win11Debloat.ps1
|
||||
PowerShell -NoProfile -ExecutionPolicy Bypass -Command "$p='%SCRIPT_PATH:'=''%'; $q=[char]34; Start-Process PowerShell -ArgumentList ('-NoProfile -ExecutionPolicy Bypass -File ' + $q + $p + $q) -Verb RunAs" >> "%logFile%" || call :Error "PowerShell command failed"
|
||||
)
|
||||
|
||||
echo.
|
||||
@@ -40,11 +38,13 @@ goto :EOF
|
||||
|
||||
:: Logging Function
|
||||
:Log
|
||||
echo %* >> "%logFile%"
|
||||
echo(%* >> "%logFile%"
|
||||
goto :EOF
|
||||
|
||||
:: Error Handler
|
||||
:Error
|
||||
echo ERROR: %*
|
||||
echo(ERROR: %*
|
||||
call :Log ERROR: %*
|
||||
echo Logged in %logFile%
|
||||
pause
|
||||
goto :EOF
|
||||
|
||||
@@ -105,7 +105,7 @@
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
FontWeight="SemiBold"
|
||||
FontFamily="Segoe Fluent Icons"
|
||||
FontFamily="{DynamicResource AppIconFontFamily}"
|
||||
Text=""
|
||||
Foreground="{DynamicResource TitleBarCloseHoverColor}"
|
||||
Margin="0,0,8,0"/>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Width" Value="46"/>
|
||||
<Setter Property="Height" Value="32"/>
|
||||
<Setter Property="FontFamily" Value="Segoe MDL2 Assets"/>
|
||||
<Setter Property="FontFamily" Value="{DynamicResource AppIconFontFamily}"/>
|
||||
<Setter Property="FontSize" Value="10"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Button.Style>
|
||||
<TextBlock Text="" FontFamily="Segoe Fluent Icons" FontSize="10"/>
|
||||
<TextBlock Text="" FontFamily="{DynamicResource AppIconFontFamily}" FontSize="10"/>
|
||||
</Button>
|
||||
|
||||
<StackPanel Grid.Row="0" Grid.RowSpan="2">
|
||||
@@ -54,7 +54,7 @@
|
||||
<!-- Loading icon (spinning) -->
|
||||
<TextBlock x:Name="ApplySpinnerIcon"
|
||||
Text=""
|
||||
FontFamily="Segoe Fluent Icons"
|
||||
FontFamily="{DynamicResource AppIconFontFamily}"
|
||||
FontSize="36"
|
||||
Foreground="{DynamicResource ButtonBgColor}"
|
||||
HorizontalAlignment="Center"
|
||||
@@ -115,7 +115,7 @@
|
||||
<!-- Success icon -->
|
||||
<TextBlock x:Name="ApplyCompletionIcon"
|
||||
Text=""
|
||||
FontFamily="Segoe Fluent Icons"
|
||||
FontFamily="{DynamicResource AppIconFontFamily}"
|
||||
FontSize="40"
|
||||
Foreground="{DynamicResource ButtonBgColor}"
|
||||
HorizontalAlignment="Center"
|
||||
@@ -141,7 +141,7 @@
|
||||
<StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,6">
|
||||
<TextBlock Text=""
|
||||
FontFamily="Segoe Fluent Icons"
|
||||
FontFamily="{DynamicResource AppIconFontFamily}"
|
||||
FontSize="14"
|
||||
Foreground="#e8912d"
|
||||
VerticalAlignment="Center"
|
||||
@@ -163,7 +163,7 @@
|
||||
Style="{DynamicResource ModalSecondaryStretchedButtonStyle}"
|
||||
AutomationProperties.Name="Support the creator">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Text="" FontFamily="Segoe Fluent Icons" FontSize="14" VerticalAlignment="Center" Margin="0,0,8,-1"/>
|
||||
<TextBlock Text="" FontFamily="{DynamicResource AppIconFontFamily}" FontSize="14" VerticalAlignment="Center" Margin="0,0,8,-1"/>
|
||||
<TextBlock Text="Support the creator" VerticalAlignment="Center" FontSize="14" Margin="0,0,0,1"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
+30
-30
@@ -65,7 +65,7 @@
|
||||
|
||||
<!-- Category header icon style -->
|
||||
<Style x:Key="CategoryHeaderIcon" TargetType="TextBlock">
|
||||
<Setter Property="FontFamily" Value="Segoe Fluent Icons"/>
|
||||
<Setter Property="FontFamily" Value="{DynamicResource AppIconFontFamily}"/>
|
||||
<Setter Property="FontSize" Value="20"/>
|
||||
<Setter Property="LineHeight" Value="20"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource AppFgColor}"/>
|
||||
@@ -196,7 +196,7 @@
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Width" Value="46"/>
|
||||
<Setter Property="Height" Value="32"/>
|
||||
<Setter Property="FontFamily" Value="Segoe Fluent Icons"/>
|
||||
<Setter Property="FontFamily" Value="{DynamicResource AppIconFontFamily}"/>
|
||||
<Setter Property="FontSize" Value="10"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
@@ -362,8 +362,8 @@
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal">
|
||||
<Button x:Name="KofiBtn" shell:WindowChrome.IsHitTestVisibleInChrome="True" Content="" FontFamily="Segoe Fluent Icons" FontSize="15" Style="{StaticResource TitlebarButton}" ToolTip="Support the creator" AutomationProperties.Name="Support the creator"/>
|
||||
<Button x:Name="MenuBtn" shell:WindowChrome.IsHitTestVisibleInChrome="True" Content="" FontFamily="Segoe Fluent Icons" FontSize="15" Style="{StaticResource TitlebarButton}" ToolTip="Options" AutomationProperties.Name="Options">
|
||||
<Button x:Name="KofiBtn" shell:WindowChrome.IsHitTestVisibleInChrome="True" Content="" FontSize="15" Style="{StaticResource TitlebarButton}" ToolTip="Support the creator" AutomationProperties.Name="Support the creator"/>
|
||||
<Button x:Name="MenuBtn" shell:WindowChrome.IsHitTestVisibleInChrome="True" Content="" FontSize="15" Style="{StaticResource TitlebarButton}" ToolTip="Options" AutomationProperties.Name="Options">
|
||||
<Button.ContextMenu>
|
||||
<ContextMenu x:Name="MainMenu">
|
||||
<ContextMenu.Resources>
|
||||
@@ -380,38 +380,38 @@
|
||||
</ContextMenu.Resources>
|
||||
<MenuItem x:Name="ImportConfigBtn" Header="Import config" AutomationProperties.Name="Import configuration">
|
||||
<MenuItem.Icon>
|
||||
<TextBlock Text="" FontFamily="Segoe Fluent Icons" FontSize="16" Foreground="{DynamicResource AppFgColor}"/>
|
||||
<TextBlock Text="" FontFamily="{DynamicResource AppIconFontFamily}" FontSize="16" Foreground="{DynamicResource AppFgColor}"/>
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem x:Name="ExportConfigBtn" Header="Export config" AutomationProperties.Name="Export configuration">
|
||||
<MenuItem.Icon>
|
||||
<TextBlock Text="" FontFamily="Segoe Fluent Icons" FontSize="16" Foreground="{DynamicResource AppFgColor}"/>
|
||||
<TextBlock Text="" FontFamily="{DynamicResource AppIconFontFamily}" FontSize="16" Foreground="{DynamicResource AppFgColor}"/>
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem x:Name="RestoreBackupBtn" Header="Restore backup" AutomationProperties.Name="Restore registry backup">
|
||||
<MenuItem.Icon>
|
||||
<TextBlock Text="" FontFamily="Segoe Fluent Icons" FontSize="16" Foreground="{DynamicResource AppFgColor}"/>
|
||||
<TextBlock Text="" FontFamily="{DynamicResource AppIconFontFamily}" FontSize="16" Foreground="{DynamicResource AppFgColor}"/>
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<Separator />
|
||||
<MenuItem x:Name="MenuDocumentation" Header="Documentation" AutomationProperties.Name="Documentation">
|
||||
<MenuItem.Icon>
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="16" Foreground="{DynamicResource AppFgColor}"/>
|
||||
<TextBlock Text="" FontFamily="{DynamicResource AppIconFontFamily}" FontSize="16" Foreground="{DynamicResource AppFgColor}"/>
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem x:Name="MenuReportBug" Header="Report a bug" AutomationProperties.Name="Report a bug">
|
||||
<MenuItem.Icon>
|
||||
<TextBlock Text="" FontFamily="Segoe Fluent Icons" FontSize="16" Foreground="{DynamicResource AppFgColor}"/>
|
||||
<TextBlock Text="" FontFamily="{DynamicResource AppIconFontFamily}" FontSize="16" Foreground="{DynamicResource AppFgColor}"/>
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem x:Name="MenuLogs" Header="Logs" AutomationProperties.Name="Logs">
|
||||
<MenuItem.Icon>
|
||||
<TextBlock Text="" FontFamily="Segoe Fluent Icons" FontSize="16" Foreground="{DynamicResource AppFgColor}"/>
|
||||
<TextBlock Text="" FontFamily="{DynamicResource AppIconFontFamily}" FontSize="16" Foreground="{DynamicResource AppFgColor}"/>
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem x:Name="MenuAbout" Header="About" AutomationProperties.Name="About">
|
||||
<MenuItem.Icon>
|
||||
<TextBlock Text="" FontFamily="Segoe Fluent Icons" FontSize="16" Foreground="{DynamicResource AppFgColor}"/>
|
||||
<TextBlock Text="" FontFamily="{DynamicResource AppIconFontFamily}" FontSize="16" Foreground="{DynamicResource AppFgColor}"/>
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
</ContextMenu>
|
||||
@@ -490,7 +490,7 @@
|
||||
<Border HorizontalAlignment="Center" BorderBrush="{DynamicResource AppBorderColor}" BorderThickness="1" CornerRadius="4" Background="{DynamicResource CardBgColor}" Padding="16,12" Width="500">
|
||||
<StackPanel>
|
||||
<TextBlock Text="What user do you want to apply changes to?" Style="{StaticResource CategoryHeaderTextBlock}"/>
|
||||
<ComboBox x:Name="UserSelectionCombo" Margin="0,0,0,6" AutomationProperties.Name="Apply Changes To">
|
||||
<ComboBox x:Name="UserSelectionCombo" Margin="0,0,0,6" AutomationProperties.Name="Apply Changes To" ToolTip="The currently logged-in user profile">
|
||||
<ComboBoxItem Content="Current User" IsSelected="True"/>
|
||||
<ComboBoxItem Content="Other User"/>
|
||||
<ComboBoxItem Content="Windows Default User (Sysprep)"/>
|
||||
@@ -506,7 +506,7 @@
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock x:Name="UsernameTextBoxPlaceholder" Grid.Column="0" Text="Enter username" Foreground="{DynamicResource AppFgColor}" Opacity="0.7" FontSize="13" Margin="3,0,0,1" VerticalAlignment="Center" IsHitTestVisible="False"/>
|
||||
<TextBox x:Name="OtherUsernameTextBox" Grid.Column="0" Style="{DynamicResource TextBoxInputStyle}" Text="" AutomationProperties.Name="Enter username"/>
|
||||
<TextBlock Grid.Column="1" Text="" FontFamily="Segoe Fluent Icons" FontSize="14" VerticalAlignment="Center" Margin="8,0,4,0" Foreground="{DynamicResource AppFgColor}" Opacity="0.7"/>
|
||||
<TextBlock Grid.Column="1" Text="" FontFamily="{DynamicResource AppIconFontFamily}" FontSize="14" VerticalAlignment="Center" Margin="8,0,4,0" Foreground="{DynamicResource AppFgColor}" Opacity="0.7"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Border>
|
||||
@@ -517,13 +517,13 @@
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,8,0,4">
|
||||
<Button x:Name="HomeDefaultModeBtn" Width="227" Height="50" Style="{DynamicResource PrimaryButtonStyle}" Margin="0,0,12,0" AutomationProperties.Name="Default Mode">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Text="" FontFamily="Segoe Fluent Icons" FontSize="16" VerticalAlignment="Center" Margin="0,0,8,-1"/>
|
||||
<TextBlock Text="" FontFamily="{DynamicResource AppIconFontFamily}" FontSize="16" VerticalAlignment="Center" Margin="0,0,8,-1"/>
|
||||
<TextBlock Text="Default Mode" ToolTip="Quickly select the recommended settings" FontWeight="SemiBold" VerticalAlignment="Center" FontSize="17" Margin="0,0,0,1"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button x:Name="HomeStartBtn" Width="227" Height="50" Style="{DynamicResource SecondaryButtonStyle}" AutomationProperties.Name="Custom Setup">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Text="" FontFamily="Segoe Fluent Icons" FontSize="14" VerticalAlignment="Center" Margin="0,0,8,-1"/>
|
||||
<TextBlock Text="" FontFamily="{DynamicResource AppIconFontFamily}" FontSize="14" VerticalAlignment="Center" Margin="0,0,8,-1"/>
|
||||
<TextBlock Text="Custom Setup" ToolTip="Manually select your preferred settings" FontWeight="SemiBold" VerticalAlignment="Center" FontSize="17" Margin="0,0,0,1"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
@@ -585,9 +585,9 @@
|
||||
</Style>
|
||||
</ToggleButton.Style>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="" FontFamily="Segoe Fluent Icons" FontSize="14" VerticalAlignment="Center" Margin="0,1,6,0"/>
|
||||
<TextBlock Text="" FontFamily="{DynamicResource AppIconFontFamily}" FontSize="14" VerticalAlignment="Center" Margin="0,1,6,0"/>
|
||||
<TextBlock Text="Quick Select" FontSize="13" VerticalAlignment="Center" Margin="0,0,6,1"/>
|
||||
<TextBlock x:Name="PresetsArrow" Text="" FontFamily="Segoe Fluent Icons" FontSize="10" VerticalAlignment="Center" RenderTransformOrigin="0.5,0.5">
|
||||
<TextBlock x:Name="PresetsArrow" Text="" FontFamily="{DynamicResource AppIconFontFamily}" FontSize="10" VerticalAlignment="Center" RenderTransformOrigin="0.5,0.5">
|
||||
<TextBlock.RenderTransform>
|
||||
<RotateTransform x:Name="PresetsArrowRotation" Angle="0"/>
|
||||
</TextBlock.RenderTransform>
|
||||
@@ -596,7 +596,7 @@
|
||||
</ToggleButton>
|
||||
<Button x:Name="ClearAppSelectionBtn" ToolTip="Clear all selected apps" Style="{DynamicResource SecondaryButtonStyle}" Height="32" Padding="10,0" Margin="0,0,10,0" AutomationProperties.Name="Clear Selection">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Text="" FontFamily="Segoe Fluent Icons" FontSize="15" VerticalAlignment="Center" Margin="0,3,6,0"/>
|
||||
<TextBlock Text="" FontFamily="{DynamicResource AppIconFontFamily}" FontSize="15" VerticalAlignment="Center" Margin="0,3,6,0"/>
|
||||
<TextBlock Text="Clear Selection" FontSize="13" VerticalAlignment="Center" Margin="0,0,0,1"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
@@ -627,7 +627,7 @@
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock x:Name="AppSearchPlaceholder" Grid.Column="0" Text="Search apps..." Foreground="{DynamicResource AppFgColor}" Opacity="0.7" FontSize="13" Margin="3,0,0,1" VerticalAlignment="Center" IsHitTestVisible="False"/>
|
||||
<TextBox x:Name="AppSearchBox" Grid.Column="0" Style="{DynamicResource TextBoxInputStyle}" Text="" AutomationProperties.Name="Search app"/>
|
||||
<TextBlock Grid.Column="1" Text="" FontFamily="Segoe Fluent Icons" FontSize="14" VerticalAlignment="Center" Margin="8,0,4,0" Foreground="{DynamicResource AppFgColor}" Opacity="0.7"/>
|
||||
<TextBlock Grid.Column="1" Text="" FontFamily="{DynamicResource AppIconFontFamily}" FontSize="14" VerticalAlignment="Center" Margin="8,0,4,0" Foreground="{DynamicResource AppFgColor}" Opacity="0.7"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Border>
|
||||
@@ -658,19 +658,19 @@
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel x:Name="HeaderNameBtn" Grid.Column="1" Orientation="Horizontal" Cursor="Hand" VerticalAlignment="Center" Style="{StaticResource SortHeaderBtnStyle}">
|
||||
<TextBlock Text="Name" FontWeight="SemiBold" FontSize="16" Foreground="{DynamicResource AppFgColor}"/>
|
||||
<TextBlock x:Name="SortArrowName" Text="" FontFamily="Segoe Fluent Icons" FontSize="11" Foreground="{DynamicResource AppFgColor}" VerticalAlignment="Center" Margin="5,1,0,0" Opacity="0.3" RenderTransformOrigin="0.5,0.5">
|
||||
<TextBlock x:Name="SortArrowName" Text="" FontFamily="{DynamicResource AppIconFontFamily}" FontSize="11" Foreground="{DynamicResource AppFgColor}" VerticalAlignment="Center" Margin="5,1,0,0" Opacity="0.3" RenderTransformOrigin="0.5,0.5">
|
||||
<TextBlock.RenderTransform><RotateTransform Angle="0"/></TextBlock.RenderTransform>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
<StackPanel x:Name="HeaderDescriptionBtn" Grid.Column="2" Orientation="Horizontal" Cursor="Hand" VerticalAlignment="Center" Margin="8,0,0,0" Style="{StaticResource SortHeaderBtnStyle}">
|
||||
<TextBlock Text="Description" FontWeight="SemiBold" FontSize="16" Foreground="{DynamicResource AppFgColor}"/>
|
||||
<TextBlock x:Name="SortArrowDescription" Text="" FontFamily="Segoe Fluent Icons" FontSize="11" Foreground="{DynamicResource AppFgColor}" VerticalAlignment="Center" Margin="5,1,0,0" Opacity="0.3" RenderTransformOrigin="0.5,0.5">
|
||||
<TextBlock x:Name="SortArrowDescription" Text="" FontFamily="{DynamicResource AppIconFontFamily}" FontSize="11" Foreground="{DynamicResource AppFgColor}" VerticalAlignment="Center" Margin="5,1,0,0" Opacity="0.3" RenderTransformOrigin="0.5,0.5">
|
||||
<TextBlock.RenderTransform><RotateTransform Angle="0"/></TextBlock.RenderTransform>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
<StackPanel x:Name="HeaderAppIdBtn" Grid.Column="3" Orientation="Horizontal" Cursor="Hand" VerticalAlignment="Center" Style="{StaticResource SortHeaderBtnStyle}">
|
||||
<TextBlock Text="App ID" FontWeight="SemiBold" FontSize="16" Foreground="{DynamicResource AppFgColor}"/>
|
||||
<TextBlock x:Name="SortArrowAppId" Text="" FontFamily="Segoe Fluent Icons" FontSize="11" Foreground="{DynamicResource AppFgColor}" VerticalAlignment="Center" Margin="5,1,0,0" Opacity="0.3" RenderTransformOrigin="0.5,0.5">
|
||||
<TextBlock x:Name="SortArrowAppId" Text="" FontFamily="{DynamicResource AppIconFontFamily}" FontSize="11" Foreground="{DynamicResource AppFgColor}" VerticalAlignment="Center" Margin="5,1,0,0" Opacity="0.3" RenderTransformOrigin="0.5,0.5">
|
||||
<TextBlock.RenderTransform><RotateTransform Angle="0"/></TextBlock.RenderTransform>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
@@ -684,7 +684,7 @@
|
||||
</ScrollViewer>
|
||||
<Border x:Name="LoadingAppsIndicator" CornerRadius="0,0,4,4" Background="{DynamicResource CardBgColor}" Opacity="0.8" Visibility="Collapsed">
|
||||
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<TextBlock Text="" FontFamily="Segoe Fluent Icons" FontSize="28" Foreground="{DynamicResource AppFgColor}" HorizontalAlignment="Center" Margin="0,0,0,8" RenderTransformOrigin="0.5,0.5">
|
||||
<TextBlock Text="" FontFamily="{DynamicResource AppIconFontFamily}" FontSize="28" Foreground="{DynamicResource AppFgColor}" HorizontalAlignment="Center" Margin="0,0,0,8" RenderTransformOrigin="0.5,0.5">
|
||||
<TextBlock.RenderTransform>
|
||||
<RotateTransform Angle="0"/>
|
||||
</TextBlock.RenderTransform>
|
||||
@@ -789,9 +789,9 @@
|
||||
</Style>
|
||||
</ToggleButton.Style>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="" FontFamily="Segoe Fluent Icons" FontSize="14" VerticalAlignment="Center" Margin="0,1,6,0"/>
|
||||
<TextBlock Text="" FontFamily="{DynamicResource AppIconFontFamily}" FontSize="14" VerticalAlignment="Center" Margin="0,1,6,0"/>
|
||||
<TextBlock Text="Quick Select" FontSize="13" VerticalAlignment="Center" Margin="0,0,6,1"/>
|
||||
<TextBlock x:Name="TweaksPresetsArrow" Text="" FontFamily="Segoe Fluent Icons" FontSize="10" VerticalAlignment="Center" RenderTransformOrigin="0.5,0.5">
|
||||
<TextBlock x:Name="TweaksPresetsArrow" Text="" FontFamily="{DynamicResource AppIconFontFamily}" FontSize="10" VerticalAlignment="Center" RenderTransformOrigin="0.5,0.5">
|
||||
<TextBlock.RenderTransform>
|
||||
<RotateTransform Angle="0"/>
|
||||
</TextBlock.RenderTransform>
|
||||
@@ -800,7 +800,7 @@
|
||||
</ToggleButton>
|
||||
<Button x:Name="ClearAllTweaksBtn" ToolTip="Clear all selected tweaks" Style="{DynamicResource SecondaryButtonStyle}" Padding="10,0" Height="32" Margin="0,0,10,0" AutomationProperties.Name="Clear Selection">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Text="" FontFamily="Segoe Fluent Icons" FontSize="15" VerticalAlignment="Center" Margin="0,3,6,0"/>
|
||||
<TextBlock Text="" FontFamily="{DynamicResource AppIconFontFamily}" FontSize="15" VerticalAlignment="Center" Margin="0,3,6,0"/>
|
||||
<TextBlock Text="Clear Selection" FontSize="13" VerticalAlignment="Center" Margin="0,0,0,1"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
@@ -832,7 +832,7 @@
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock x:Name="TweakSearchPlaceholder" Grid.Column="0" Text="Search tweaks..." Foreground="{DynamicResource AppFgColor}" Opacity="0.7" FontSize="13" Margin="3,0,0,1" VerticalAlignment="Center" IsHitTestVisible="False"/>
|
||||
<TextBox x:Name="TweakSearchBox" Grid.Column="0" Style="{DynamicResource TextBoxInputStyle}" Text="" AutomationProperties.Name="Search tweaks"/>
|
||||
<TextBlock Grid.Column="1" Text="" FontFamily="Segoe Fluent Icons" FontSize="14" VerticalAlignment="Center" Margin="8,0,4,0" Foreground="{DynamicResource AppFgColor}" Opacity="0.7"/>
|
||||
<TextBlock Grid.Column="1" Text="" FontFamily="{DynamicResource AppIconFontFamily}" FontSize="14" VerticalAlignment="Center" Margin="8,0,4,0" Foreground="{DynamicResource AppFgColor}" Opacity="0.7"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Border>
|
||||
@@ -986,7 +986,7 @@
|
||||
</Button>
|
||||
<Button x:Name="DeploymentApplyBtn" Style="{DynamicResource PrimaryButtonStyle}" Width="200" Height="44" HorizontalAlignment="Center" AutomationProperties.Name="Apply Changes">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Text="" FontFamily="Segoe Fluent Icons" FontSize="20" FontWeight="SemiBold" VerticalAlignment="Center"/>
|
||||
<TextBlock Text="" FontFamily="{DynamicResource AppIconFontFamily}" FontSize="20" FontWeight="SemiBold" VerticalAlignment="Center"/>
|
||||
<TextBlock Text="Apply Changes" VerticalAlignment="Center" FontSize="18" FontWeight="SemiBold" Margin="8,0,0,4"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
@@ -1006,7 +1006,7 @@
|
||||
</Grid.ColumnDefinitions>
|
||||
<Button x:Name="PreviousBtn" Grid.Column="0" Width="120" Height="36" Style="{DynamicResource SecondaryButtonStyle}" Visibility="Collapsed" AutomationProperties.Name="Back">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Text="" FontFamily="Segoe Fluent Icons" FontSize="12" Margin="0,0,8,0" VerticalAlignment="Center"/>
|
||||
<TextBlock Text="" FontFamily="{DynamicResource AppIconFontFamily}" FontSize="12" Margin="0,0,8,0" VerticalAlignment="Center"/>
|
||||
<TextBlock Text="Back" VerticalAlignment="Center" FontSize="14" Margin="0,0,0,3"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
@@ -1015,7 +1015,7 @@
|
||||
<Button x:Name="NextBtn" Width="120" Height="36" Style="{DynamicResource PrimaryButtonStyle}" AutomationProperties.Name="Next">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Text="Next" VerticalAlignment="Center" FontSize="14" Margin="0,0,0,3"/>
|
||||
<TextBlock Text="" FontFamily="Segoe Fluent Icons" FontSize="12" Margin="8,0,0,0" VerticalAlignment="Center"/>
|
||||
<TextBlock Text="" FontFamily="{DynamicResource AppIconFontFamily}" FontSize="12" Margin="8,0,0,0" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
<!-- Icon -->
|
||||
<TextBlock x:Name="IconText"
|
||||
Grid.Column="0"
|
||||
FontFamily="Segoe Fluent Icons"
|
||||
FontFamily="{DynamicResource AppIconFontFamily}"
|
||||
FontSize="24"
|
||||
Foreground="{DynamicResource AppFgColor}"
|
||||
VerticalAlignment="Center"
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Button.Style>
|
||||
<TextBlock Text="" FontFamily="Segoe Fluent Icons" FontSize="10"/>
|
||||
<TextBlock Text="" FontFamily="{DynamicResource AppIconFontFamily}" FontSize="10"/>
|
||||
</Button>
|
||||
|
||||
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto" Margin="0">
|
||||
@@ -135,7 +135,7 @@
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Column="0"
|
||||
Text=""
|
||||
FontFamily="Segoe Fluent Icons"
|
||||
FontFamily="{DynamicResource AppIconFontFamily}"
|
||||
FontSize="24"
|
||||
VerticalAlignment="Center"
|
||||
Margin="14,0,14,0"/>
|
||||
@@ -165,7 +165,7 @@
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Column="0"
|
||||
Text=""
|
||||
FontFamily="Segoe Fluent Icons"
|
||||
FontFamily="{DynamicResource AppIconFontFamily}"
|
||||
FontSize="24"
|
||||
VerticalAlignment="Center"
|
||||
Margin="14,0,14,0"/>
|
||||
|
||||
@@ -272,7 +272,7 @@
|
||||
<Border x:Name="Border" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="4">
|
||||
<TextBlock x:Name="Arrow"
|
||||
Text=""
|
||||
FontFamily="Segoe Fluent Icons"
|
||||
FontFamily="{DynamicResource AppIconFontFamily}"
|
||||
FontSize="10"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
@@ -393,12 +393,12 @@
|
||||
</Grid.ColumnDefinitions>
|
||||
<Border x:Name="CheckBoxBorder" Grid.Column="0" Width="20" Height="20" Background="{DynamicResource CheckBoxBgColor}" BorderBrush="{DynamicResource CheckBoxBorderColor}" BorderThickness="1" CornerRadius="4" Margin="0,0,8,0">
|
||||
<Grid>
|
||||
<TextBlock x:Name="CheckMark" Text="" FontFamily="Segoe Fluent Icons" FontSize="13" FontWeight="SemiBold" Foreground="{DynamicResource ButtonBgColor}" HorizontalAlignment="Center" VerticalAlignment="Center" Opacity="0">
|
||||
<TextBlock x:Name="CheckMark" Text="" FontFamily="{DynamicResource AppIconFontFamily}" FontSize="13" FontWeight="SemiBold" Foreground="{DynamicResource ButtonBgColor}" HorizontalAlignment="Center" VerticalAlignment="Center" Opacity="0">
|
||||
<TextBlock.Clip>
|
||||
<RectangleGeometry x:Name="CheckMarkClip" Rect="0,0,0,16"/>
|
||||
</TextBlock.Clip>
|
||||
</TextBlock>
|
||||
<TextBlock x:Name="IndeterminateMark" Text="" FontFamily="Segoe Fluent Icons" FontSize="13" FontWeight="Bold" Foreground="{DynamicResource ButtonBgColor}" HorizontalAlignment="Center" VerticalAlignment="Center" Opacity="0" Margin="1,0,0,1">
|
||||
<TextBlock x:Name="IndeterminateMark" Text="" FontFamily="{DynamicResource AppIconFontFamily}" FontSize="13" FontWeight="Bold" Foreground="{DynamicResource ButtonBgColor}" HorizontalAlignment="Center" VerticalAlignment="Center" Opacity="0" Margin="1,0,0,1">
|
||||
<TextBlock.Clip>
|
||||
<RectangleGeometry x:Name="IndeterminateMarkClip" Rect="0,0,0,12"/>
|
||||
</TextBlock.Clip>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
# Restart the Windows Explorer process
|
||||
function RestartExplorer {
|
||||
# Restarting Explorer while running in Sysprep or User context is not necessary
|
||||
if ($script:Params.ContainsKey("Sysprep") -or $script:Params.ContainsKey("User")) {
|
||||
return
|
||||
}
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Restarts Windows Explorer to apply system changes.
|
||||
|
||||
.DESCRIPTION
|
||||
Restarts the Explorer process to ensure all UI modifications take effect. Shows a warning if any of the applied features require a reboot to take full effect.
|
||||
#>
|
||||
function RestartExplorer {
|
||||
if ($script:Params.ContainsKey("WhatIf")) {
|
||||
Write-Host "[WhatIf] Restart the Windows Explorer process" -ForegroundColor Cyan
|
||||
return
|
||||
@@ -17,11 +18,9 @@ function RestartExplorer {
|
||||
return
|
||||
}
|
||||
|
||||
foreach ($paramKey in $script:Params.Keys) {
|
||||
if ($script:Features.ContainsKey($paramKey) -and $script:Features[$paramKey].RequiresReboot -eq $true) {
|
||||
$feature = $script:Features[$paramKey]
|
||||
Write-Host "Warning: '$($feature.Label)' requires a reboot to take full effect" -ForegroundColor Yellow
|
||||
}
|
||||
$rebootFeatures = Get-RebootFeatureLabels
|
||||
foreach ($displayLabel in $rebootFeatures) {
|
||||
Write-Host "Warning: '$displayLabel' requires a reboot to take full effect" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
# Only restart if the powershell process matches the OS architecture.
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Loads a registry backup from a JSON file and normalizes its contents.
|
||||
|
||||
.DESCRIPTION
|
||||
Loads a registry backup from disk and returns a normalized representation
|
||||
of its contents suitable for use by the restore workflow. Throws if the
|
||||
file is missing, unreadable, or not valid JSON.
|
||||
|
||||
.PARAMETER FilePath
|
||||
The absolute path to the registry backup JSON file to load.
|
||||
|
||||
.OUTPUTS
|
||||
PSCustomObject
|
||||
A normalized registry backup object produced by Normalize-RegistryBackup.
|
||||
#>
|
||||
function Load-RegistryBackupFromFile {
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
@@ -18,6 +34,24 @@ function Load-RegistryBackupFromFile {
|
||||
return Normalize-RegistryBackup -Backup $rawBackup
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Validates and normalizes a raw registry backup object.
|
||||
|
||||
.DESCRIPTION
|
||||
Validates the structure and content of the supplied backup and converts
|
||||
it into a normalized representation that can be safely consumed by the
|
||||
restore workflow. Throws if validation fails.
|
||||
|
||||
.PARAMETER Backup
|
||||
The raw backup object (typically parsed from JSON) to normalize.
|
||||
|
||||
.OUTPUTS
|
||||
PSCustomObject
|
||||
A normalized backup with Version, BackupType, CreatedAt, CreatedBy,
|
||||
ComputerName, Target, SelectedFeatures, SelectedUndoFeatures, and
|
||||
RegistryKeys properties.
|
||||
#>
|
||||
function Normalize-RegistryBackup {
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
@@ -59,7 +93,8 @@ function Normalize-RegistryBackup {
|
||||
}
|
||||
elseif ($normalizedTarget -like 'CurrentUser:*') {
|
||||
$targetCurrentUserName = $normalizedTarget.Substring(12)
|
||||
if ([string]::IsNullOrWhiteSpace($targetCurrentUserName) -or ($targetCurrentUserName -ne $env:USERNAME)) {
|
||||
if ([string]::IsNullOrWhiteSpace($targetCurrentUserName) -or
|
||||
-not (Test-UserNameMatch -UserNameA $targetCurrentUserName -UserNameB $env:USERNAME)) {
|
||||
$errors.Add("Backup was made for '$targetCurrentUserName', this does not match current user '$env:USERNAME'.")
|
||||
}
|
||||
}
|
||||
@@ -97,10 +132,17 @@ function Normalize-RegistryBackup {
|
||||
if ($allSelectedFeatures.Count -eq 0) {
|
||||
$errors.Add('Backup must contain at least one feature ID in SelectedFeatures or SelectedUndoFeatures.')
|
||||
}
|
||||
else {
|
||||
try {
|
||||
$allowListValidationErrors = @(Test-RegistryBackupMatchesSelectedFeatures -SelectedFeatureIds @($selectedFeatures) -SelectedUndoFeatureIds @($selectedUndoFeatures) -Target $normalizedTarget -RegistryKeys @($normalizedKeys))
|
||||
foreach ($allowListValidationError in $allowListValidationErrors) {
|
||||
$errors.Add([string]$allowListValidationError)
|
||||
}
|
||||
}
|
||||
catch {
|
||||
$errors.Add("Failed to validate backup: $($_.Exception.Message)")
|
||||
}
|
||||
}
|
||||
|
||||
if ($errors.Count -gt 0) {
|
||||
Write-Error "Backup validation failed: $($errors -join ' ')"
|
||||
@@ -125,6 +167,23 @@ function Normalize-RegistryBackup {
|
||||
}
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Restores registry state from a normalized backup object.
|
||||
|
||||
.DESCRIPTION
|
||||
Applies the registry state described by the supplied backup back to the
|
||||
registry, loading the appropriate user hive when required.
|
||||
|
||||
.PARAMETER Backup
|
||||
A normalized backup object (as produced by Normalize-RegistryBackup) whose
|
||||
RegistryKeys snapshots should be restored.
|
||||
|
||||
.OUTPUTS
|
||||
PSCustomObject
|
||||
Returns an object with a Result property set to $true when the restore
|
||||
completes successfully.
|
||||
#>
|
||||
function Restore-RegistryBackupState {
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
|
||||
@@ -158,9 +158,14 @@ function EnableStoreSearchSuggestions {
|
||||
$acl = Get-Acl -Path $StoreAppsDatabase
|
||||
$denyRules = @(
|
||||
$acl.Access | Where-Object {
|
||||
$_.AccessControlType -eq [System.Security.AccessControl.AccessControlType]::Deny -and
|
||||
(($_.FileSystemRights -band [System.Security.AccessControl.FileSystemRights]::FullControl) -ne 0) -and
|
||||
(try { $_.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]) -eq $everyoneSid } catch { $false })
|
||||
if ($_.AccessControlType -ne [System.Security.AccessControl.AccessControlType]::Deny) { return $false }
|
||||
if (($_.FileSystemRights -band [System.Security.AccessControl.FileSystemRights]::FullControl) -eq 0) { return $false }
|
||||
try {
|
||||
return ($_.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]) -eq $everyoneSid)
|
||||
}
|
||||
catch {
|
||||
return $false
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -13,6 +13,13 @@ function Get-UndoFeatureLabel {
|
||||
return [string]$script:FeatureLabelLookup[$FeatureId]
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Returns the tweak actions that are pending based on the current UI state.
|
||||
|
||||
.OUTPUTS
|
||||
[PSCustomObject[]] Objects with Action, FeatureId, and Label properties.
|
||||
#>
|
||||
function Get-PendingTweakActions {
|
||||
param(
|
||||
[System.Windows.Window]$Window,
|
||||
@@ -54,7 +61,7 @@ function Get-PendingTweakActions {
|
||||
$actions.Add([PSCustomObject]@{
|
||||
Action = 'Undo'
|
||||
FeatureId = [string]$mapping.FeatureId
|
||||
Label = [string]$script:FeatureLabelLookup[$mapping.FeatureId]
|
||||
Label = [string](Get-UndoFeatureLabel -FeatureId $mapping.FeatureId)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -435,6 +442,9 @@ function Update-UserSelectionDescription {
|
||||
$UserSelectionDescription.Text = "The default user template, affecting all new users created after this point. Useful for Sysprep deployment."
|
||||
}
|
||||
}
|
||||
|
||||
# Mirror the description text on the combo's tooltip so the same context is shown on hover.
|
||||
$UserSelectionCombo.ToolTip = $UserSelectionDescription.Text
|
||||
}
|
||||
|
||||
function Test-OtherUsername {
|
||||
|
||||
@@ -132,6 +132,9 @@ function Build-DynamicTweaks {
|
||||
# Convert HTML entity to character (e.g.,  -> actual character)
|
||||
if ($categoryIcon -match '&#x([0-9A-Fa-f]+);') {
|
||||
$hexValue = [Convert]::ToInt32($matches[1], 16)
|
||||
if ($WinVersion -lt 22000 -and $hexValue -eq 0xE794) {
|
||||
$hexValue = 0xE734
|
||||
}
|
||||
$icon.Text = [char]$hexValue
|
||||
}
|
||||
$icon.Style = $Window.Resources['CategoryHeaderIcon']
|
||||
@@ -201,9 +204,8 @@ function Build-DynamicTweaks {
|
||||
foreach ($categoryObj in $orderedCategories) {
|
||||
$categoryName = $categoryObj.Name
|
||||
|
||||
# Create/get card for this category
|
||||
$panel = GetOrCreateCategoryCard -categoryObj $categoryObj
|
||||
if (-not $panel) { continue }
|
||||
# Card is created lazily on the first rendered item
|
||||
$panel = $null
|
||||
|
||||
# Collect groups and features for this category, then sort by priority
|
||||
$categoryItems = @()
|
||||
@@ -287,6 +289,7 @@ function Build-DynamicTweaks {
|
||||
if ($soleFeature.FeatureId -match '^Disable') { $opt = 'Disable' } elseif ($soleFeature.FeatureId -match '^Enable') { $opt = 'Enable' }
|
||||
$items = @('No Change', $opt)
|
||||
$comboName = ("Feature_{0}_Combo" -f $soleFeature.FeatureId) -replace '[^a-zA-Z0-9_]', ''
|
||||
if (-not $panel) { $panel = GetOrCreateCategoryCard -categoryObj $categoryObj }
|
||||
$combo = CreateLabeledCombo -parent $panel -labelText $soleFeature.Label -comboName $comboName -items $items
|
||||
# attach tooltip from Features.json if present
|
||||
if ($soleFeature.ToolTip -or $soleFeature.DisableWhenApplied -eq $true) {
|
||||
@@ -311,6 +314,7 @@ function Build-DynamicTweaks {
|
||||
|
||||
$items = @('No Change') + ($filteredValues | ForEach-Object { $_.Label })
|
||||
$comboName = 'Group_{0}Combo' -f $group.GroupId
|
||||
if (-not $panel) { $panel = GetOrCreateCategoryCard -categoryObj $categoryObj }
|
||||
$combo = CreateLabeledCombo -parent $panel -labelText $group.Label -comboName $comboName -items $items
|
||||
# attach tooltip from UiGroups if present
|
||||
if ($group.ToolTip) {
|
||||
@@ -331,6 +335,7 @@ function Build-DynamicTweaks {
|
||||
if ($feature.FeatureId -match '^Disable') { $opt = 'Disable' } elseif ($feature.FeatureId -match '^Enable') { $opt = 'Enable' }
|
||||
$items = @('No Change', $opt)
|
||||
$comboName = ("Feature_{0}_Combo" -f $feature.FeatureId) -replace '[^a-zA-Z0-9_]', ''
|
||||
if (-not $panel) { $panel = GetOrCreateCategoryCard -categoryObj $categoryObj }
|
||||
$combo = CreateLabeledCombo -parent $panel -labelText $feature.Label -comboName $comboName -items $items
|
||||
# attach tooltip from Features.json if present, and include the disabled-state reason
|
||||
if ($feature.ToolTip -or $feature.DisableWhenApplied -eq $true) {
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
populates the window's Resources with SolidColorBrush entries keyed by
|
||||
category and resource name (e.g. "AppAccentColor"). Additionally loads and
|
||||
merges shared XAML styles from the script's SharedStylesSchema path if
|
||||
available.
|
||||
available. Also resolves the icon font: Segoe Fluent Icons on Windows 11
|
||||
and Segoe MDL2 Assets on Windows 10.
|
||||
|
||||
.PARAMETER window
|
||||
The WPF Window whose resource dictionary will be populated.
|
||||
@@ -122,6 +123,12 @@ function SetWindowThemeResources {
|
||||
}
|
||||
}
|
||||
|
||||
# Segoe Fluent Icons ships only on Windows 11 (build >= 22000).
|
||||
# On Windows 10, fall back to Segoe MDL2 Assets.
|
||||
$winBuild = Get-ItemPropertyValue 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' CurrentBuild
|
||||
$iconFontName = if ($winBuild -ge 22000) { 'Segoe Fluent Icons' } else { 'Segoe MDL2 Assets' }
|
||||
$window.Resources['AppIconFontFamily'] = [System.Windows.Media.FontFamily]::new($iconFontName)
|
||||
|
||||
# Load and merge shared styles
|
||||
if ($script:SharedStylesSchema -and (Test-Path $script:SharedStylesSchema)) {
|
||||
$sharedXaml = Get-Content -Path $script:SharedStylesSchema -Raw
|
||||
|
||||
@@ -144,13 +144,7 @@ function Show-ApplyModal {
|
||||
|
||||
# Show completion message with reboot instructions if any applied features require reboot
|
||||
if ($RestartExplorer) {
|
||||
$rebootFeatures = @()
|
||||
foreach ($paramKey in $script:Params.Keys) {
|
||||
if ($script:Features.ContainsKey($paramKey) -and $script:Features[$paramKey].RequiresReboot -eq $true) {
|
||||
$feature = $script:Features[$paramKey]
|
||||
$rebootFeatures += "$($feature.Label)"
|
||||
}
|
||||
}
|
||||
$rebootFeatures = Get-RebootFeatureLabels
|
||||
|
||||
if ($rebootFeatures.Count -gt 0) {
|
||||
foreach ($featureName in $rebootFeatures) {
|
||||
|
||||
@@ -1,3 +1,24 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Displays the Restore Backup wizard dialog.
|
||||
|
||||
.DESCRIPTION
|
||||
Presents a modal wizard that lets the user choose and restore either a
|
||||
registry backup or a Start Menu pinned-apps backup. Returns the user's
|
||||
selection via $window.Tag.
|
||||
|
||||
.PARAMETER Owner
|
||||
Optional parent WPF Window used to host this modal dialog. Defaults to the
|
||||
shared $script:GuiWindow when not supplied.
|
||||
|
||||
.OUTPUTS
|
||||
Hashtable
|
||||
Returns a Hashtable describing the user's choice. Possible shapes:
|
||||
RestoreRegistry - @{ Result='RestoreRegistry'; Backup=<normalizedBackup> }
|
||||
RestoreStartMenu - @{ Result='RestoreStartMenu'; StartMenuScope=<scope>;
|
||||
UseManualBackupFile=<bool>; BackupFilePath=<path|string> }
|
||||
Cancelled - @{ Result='Cancelled' } (from New-RestoreDialogState)
|
||||
#>
|
||||
function Show-RestoreBackupDialog {
|
||||
param(
|
||||
[System.Windows.Window]$Owner = $null
|
||||
@@ -295,11 +316,18 @@ function Show-RestoreBackupDialog {
|
||||
}
|
||||
|
||||
Write-Host "Backup file selected: $($openDialog.FileName)"
|
||||
|
||||
try {
|
||||
$selectedBackup = Load-RegistryBackupFromFile -FilePath $openDialog.FileName
|
||||
|
||||
if (-not (& $showRegistryOverview -SelectedBackup $selectedBackup -SelectedBackupFilePath $openDialog.FileName)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Show-MessageBox -Owner $window -Title 'Invalid Backup File' -Message "The selected file could not be loaded:`n$($_.Exception.Message)" -Button 'OK' -Icon 'Error' | Out-Null
|
||||
return
|
||||
}
|
||||
|
||||
$state.SelectedRegistryBackup = $selectedBackup
|
||||
$primaryActionBtn.Content = 'Restore from backup'
|
||||
|
||||
@@ -1,231 +0,0 @@
|
||||
param (
|
||||
[switch]$CLI,
|
||||
[switch]$Silent,
|
||||
[switch]$Verbose,
|
||||
[switch]$Sysprep,
|
||||
[string]$LogPath,
|
||||
[string]$User,
|
||||
[switch]$NoRestartExplorer,
|
||||
[switch]$CreateRestorePoint,
|
||||
[switch]$RunDefaults,
|
||||
[switch]$RunDefaultsLite,
|
||||
[switch]$RunSavedSettings,
|
||||
[string]$Config,
|
||||
[string]$Apps,
|
||||
[string]$AppRemovalTarget,
|
||||
[switch]$RemoveApps,
|
||||
[switch]$RemoveGamingApps,
|
||||
[switch]$RemoveHPApps,
|
||||
[switch]$ForceRemoveEdge,
|
||||
[switch]$DisableDVR,
|
||||
[switch]$DisableGameBarIntegration,
|
||||
[switch]$EnableWindowsSandbox,
|
||||
[switch]$EnableWindowsSubsystemForLinux,
|
||||
[switch]$DisableTelemetry,
|
||||
[switch]$DisableSearchHistory,
|
||||
[switch]$DisableFastStartup,
|
||||
[switch]$DisableBitlockerAutoEncryption,
|
||||
[switch]$DisableModernStandbyNetworking,
|
||||
[switch]$DisableStorageSense,
|
||||
[switch]$DisableUpdateASAP,
|
||||
[switch]$PreventUpdateAutoReboot,
|
||||
[switch]$DisableDeliveryOptimization,
|
||||
[switch]$DisableBing,
|
||||
[switch]$DisableStoreSearchSuggestions,
|
||||
[switch]$DisableDesktopSpotlight,
|
||||
[switch]$DisableLockscreenTips,
|
||||
[switch]$DisableSuggestions,
|
||||
[switch]$DisableLocationServices,
|
||||
[switch]$DisableFindMyDevice,
|
||||
[switch]$DisableEdgeAds,
|
||||
[switch]$DisableBraveBloat,
|
||||
[switch]$DisableSettings365Ads,
|
||||
[switch]$DisableSettingsHome,
|
||||
[switch]$ShowHiddenFolders,
|
||||
[switch]$ShowKnownFileExt,
|
||||
[switch]$HideDupliDrive,
|
||||
[switch]$EnableDarkMode,
|
||||
[switch]$DisableTransparency,
|
||||
[switch]$DisableAnimations,
|
||||
[switch]$TaskbarAlignLeft,
|
||||
[switch]$CombineTaskbarAlways, [switch]$CombineTaskbarWhenFull, [switch]$CombineTaskbarNever,
|
||||
[switch]$CombineMMTaskbarAlways, [switch]$CombineMMTaskbarWhenFull, [switch]$CombineMMTaskbarNever,
|
||||
[switch]$MMTaskbarModeAll, [switch]$MMTaskbarModeMainActive, [switch]$MMTaskbarModeActive,
|
||||
[switch]$HideSearchTb, [switch]$ShowSearchIconTb, [switch]$ShowSearchLabelTb, [switch]$ShowSearchBoxTb,
|
||||
[switch]$HideTaskview,
|
||||
[switch]$DisableStartRecommended,
|
||||
[switch]$DisableStartAllApps, [switch]$StartAllAppsCategory, [switch]$StartAllAppsGrid, [switch]$StartAllAppsList,
|
||||
[switch]$DisableStartPhoneLink,
|
||||
[switch]$DisableCopilot,
|
||||
[switch]$DisableRecall,
|
||||
[switch]$DisableClickToDo,
|
||||
[switch]$DisableAISvcAutoStart,
|
||||
[switch]$DisablePaintAI,
|
||||
[switch]$DisableNotepadAI,
|
||||
[switch]$DisableEdgeAI,
|
||||
[switch]$DisableSearchHighlights,
|
||||
[switch]$DisableWidgets,
|
||||
[switch]$HideChat,
|
||||
[switch]$EnableEndTask,
|
||||
[switch]$EnableLastActiveClick,
|
||||
[switch]$ClearStart,
|
||||
[string]$ReplaceStart,
|
||||
[switch]$ClearStartAllUsers,
|
||||
[string]$ReplaceStartAllUsers,
|
||||
[switch]$RevertContextMenu,
|
||||
[switch]$DisableDragTray,
|
||||
[switch]$DisableMouseAcceleration,
|
||||
[switch]$DisableStickyKeys,
|
||||
[switch]$DisableWindowSnapping,
|
||||
[switch]$DisableSnapAssist,
|
||||
[switch]$DisableSnapLayouts,
|
||||
[switch]$HideTabsInAltTab, [switch]$Show3TabsInAltTab, [switch]$Show5TabsInAltTab, [switch]$Show20TabsInAltTab,
|
||||
[switch]$HideHome,
|
||||
[switch]$HideGallery,
|
||||
[switch]$ExplorerToHome,
|
||||
[switch]$ExplorerToThisPC,
|
||||
[switch]$ExplorerToDownloads,
|
||||
[switch]$ExplorerToOneDrive,
|
||||
[switch]$AddFoldersToThisPC,
|
||||
[switch]$HideOnedrive,
|
||||
[switch]$Hide3dObjects,
|
||||
[switch]$HideMusic,
|
||||
[switch]$HideIncludeInLibrary,
|
||||
[switch]$HideGiveAccessTo,
|
||||
[switch]$HideShare,
|
||||
[switch]$ShowDriveLettersFirst,
|
||||
[switch]$ShowDriveLettersLast,
|
||||
[switch]$ShowNetworkDriveLettersFirst,
|
||||
[switch]$HideDriveLetters
|
||||
)
|
||||
|
||||
# Show error if current powershell environment does not have LanguageMode set to FullLanguage
|
||||
if ($ExecutionContext.SessionState.LanguageMode -ne "FullLanguage") {
|
||||
Write-Host "Error: Win11Debloat is unable to run on your system. PowerShell execution is restricted by security policies" -ForegroundColor Red
|
||||
Write-Output ""
|
||||
Write-Output "Press enter to exit..."
|
||||
Read-Host | Out-Null
|
||||
Exit
|
||||
}
|
||||
|
||||
Clear-Host
|
||||
Write-Output "-------------------------------------------------------------------------------------------"
|
||||
Write-Output " Win11Debloat Script - Get Dev"
|
||||
Write-Output "-------------------------------------------------------------------------------------------"
|
||||
|
||||
$tempRootPath = $env:TEMP
|
||||
$tempWorkPath = Join-Path $tempRootPath 'Win11Debloat'
|
||||
$tempArchivePath = Join-Path $tempRootPath 'win11debloat.zip'
|
||||
|
||||
Write-Output "> Downloading Win11Debloat for development..."
|
||||
|
||||
# Download latest version of Win11Debloat from GitHub master branch as zip archive
|
||||
try {
|
||||
Invoke-RestMethod "https://github.com/Raphire/Win11Debloat/archive/refs/heads/master.zip" -OutFile $tempArchivePath
|
||||
}
|
||||
catch {
|
||||
Write-Host "Error: Unable to fetch master branch from GitHub. Please check your internet connection and try again." -ForegroundColor Red
|
||||
Write-Output ""
|
||||
Write-Output "Press enter to exit..."
|
||||
Read-Host | Out-Null
|
||||
Exit
|
||||
}
|
||||
|
||||
# Remove old script folder if it exists, but keep configs, logs and backups
|
||||
if (Test-Path $tempWorkPath) {
|
||||
Write-Output ""
|
||||
Write-Output "> Cleaning up old Win11Debloat folder..."
|
||||
|
||||
Get-ChildItem -Path $tempWorkPath -Exclude Config,Logs,Backups | Remove-Item -Recurse -Force
|
||||
}
|
||||
|
||||
$configDir = Join-Path $tempWorkPath 'Config'
|
||||
$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
|
||||
if (Test-Path "$configDir") {
|
||||
Write-Output ""
|
||||
Write-Output "> Backing up existing config files..."
|
||||
|
||||
New-Item -ItemType Directory -Path "$backupDir" -Force | Out-Null
|
||||
|
||||
$filesToKeep = @(
|
||||
'LastUsedSettings.json'
|
||||
)
|
||||
|
||||
Get-ChildItem -Path "$configDir" -Recurse | Where-Object { $_.Name -in $filesToKeep } | Move-Item -Destination "$backupDir"
|
||||
|
||||
Remove-Item "$configDir" -Recurse -Force
|
||||
}
|
||||
|
||||
Write-Output ""
|
||||
Write-Output "> Unpacking..."
|
||||
|
||||
# Unzip archive to Win11Debloat folder
|
||||
Expand-Archive $tempArchivePath $tempWorkPath
|
||||
|
||||
# Remove archive
|
||||
Remove-Item $tempArchivePath
|
||||
|
||||
# Move files
|
||||
Get-ChildItem -Path (Join-Path $tempWorkPath '*Win11Debloat-*') -Recurse | Move-Item -Destination $tempWorkPath
|
||||
|
||||
# Add existing config files back to Config folder
|
||||
if (Test-Path "$backupDir") {
|
||||
if (-not (Test-Path "$configDir")) {
|
||||
New-Item -ItemType Directory -Path "$configDir" -Force | Out-Null
|
||||
}
|
||||
|
||||
Write-Output ""
|
||||
Write-Output "> Restoring existing config files..."
|
||||
|
||||
Get-ChildItem -Path "$backupDir" -Recurse | Move-Item -Destination "$configDir"
|
||||
Remove-Item "$backupDir" -Recurse -Force
|
||||
}
|
||||
|
||||
# Make list of arguments to pass on to the script
|
||||
$arguments = $($PSBoundParameters.GetEnumerator() | ForEach-Object {
|
||||
if ($_.Value -eq $true) {
|
||||
"-$($_.Key)"
|
||||
}
|
||||
else {
|
||||
"-$($_.Key) ""$($_.Value)"""
|
||||
}
|
||||
})
|
||||
|
||||
Write-Output ""
|
||||
Write-Output "> Launching Win11Debloat..."
|
||||
|
||||
# Minimize the powershell window when no parameters are provided
|
||||
if ($arguments.Count -eq 0) {
|
||||
$windowStyle = "Minimized"
|
||||
}
|
||||
else {
|
||||
$windowStyle = "Normal"
|
||||
}
|
||||
|
||||
# Remove Powershell 7 modules from path to prevent module loading issues in the script
|
||||
if ($PSVersionTable.PSVersion.Major -ge 7) {
|
||||
$NewPSModulePath = $env:PSModulePath -split ';' | Where-Object -FilterScript { $_ -like '*WindowsPowerShell*' }
|
||||
$env:PSModulePath = $NewPSModulePath -join ';'
|
||||
}
|
||||
|
||||
# Run Win11Debloat script with the provided arguments
|
||||
$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
|
||||
if ($null -ne $debloatProcess) {
|
||||
$debloatProcess.WaitForExit()
|
||||
}
|
||||
|
||||
# Remove all remaining script files, except for configs, logs and backups
|
||||
if (Test-Path $tempWorkPath) {
|
||||
Write-Output ""
|
||||
Write-Output "> Cleaning up..."
|
||||
|
||||
# Cleanup, remove Win11Debloat directory
|
||||
Get-ChildItem -Path $tempWorkPath -Exclude Config,Logs,Backups | Remove-Item -Recurse -Force
|
||||
}
|
||||
|
||||
Write-Output ""
|
||||
+17
-9
@@ -1,7 +1,9 @@
|
||||
param (
|
||||
[switch]$Verbose,
|
||||
[switch]$WhatIf,
|
||||
[switch]$Dev,
|
||||
[switch]$CLI,
|
||||
[switch]$Silent,
|
||||
[switch]$Verbose,
|
||||
[switch]$Sysprep,
|
||||
[string]$LogPath,
|
||||
[string]$User,
|
||||
@@ -26,10 +28,12 @@ param (
|
||||
[switch]$DisableFastStartup,
|
||||
[switch]$DisableBitlockerAutoEncryption,
|
||||
[switch]$DisableModernStandbyNetworking,
|
||||
[switch]$DisableNotifications,
|
||||
[switch]$DisableStorageSense,
|
||||
[switch]$DisableUpdateASAP,
|
||||
[switch]$PreventUpdateAutoReboot,
|
||||
[switch]$DisableDeliveryOptimization,
|
||||
[switch]$DisableDeviceAutoAppDownload,
|
||||
[switch]$DisableBing,
|
||||
[switch]$DisableStoreSearchSuggestions,
|
||||
[switch]$DisableDesktopSpotlight,
|
||||
@@ -110,7 +114,7 @@ if ($ExecutionContext.SessionState.LanguageMode -ne "FullLanguage") {
|
||||
|
||||
Clear-Host
|
||||
Write-Output "-------------------------------------------------------------------------------------------"
|
||||
Write-Output " Win11Debloat Script - Get"
|
||||
Write-Output " Win11Debloat Script"
|
||||
Write-Output "-------------------------------------------------------------------------------------------"
|
||||
|
||||
$tempRootPath = $env:TEMP
|
||||
@@ -119,13 +123,17 @@ $tempArchivePath = Join-Path $tempRootPath 'win11debloat.zip'
|
||||
|
||||
Write-Output "> Downloading Win11Debloat..."
|
||||
|
||||
# Download latest version of Win11Debloat from GitHub as zip archive
|
||||
# Download Win11Debloat from GitHub as a zip archive.
|
||||
try {
|
||||
$LatestReleaseUri = (Invoke-RestMethod https://api.github.com/repos/Raphire/Win11Debloat/releases/latest).zipball_url
|
||||
Invoke-RestMethod $LatestReleaseUri -OutFile $tempArchivePath
|
||||
if ($Dev) {
|
||||
$sourceUri = "https://github.com/Raphire/Win11Debloat/archive/refs/heads/master.zip"
|
||||
} else {
|
||||
$sourceUri = (Invoke-RestMethod https://api.github.com/repos/Raphire/Win11Debloat/releases/latest).zipball_url
|
||||
}
|
||||
Invoke-RestMethod $sourceUri -OutFile $tempArchivePath
|
||||
}
|
||||
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 required files from GitHub. Please check your internet connection and try again." -ForegroundColor Red
|
||||
Write-Output ""
|
||||
Write-Output "Press enter to exit..."
|
||||
Read-Host | Out-Null
|
||||
@@ -135,7 +143,7 @@ catch {
|
||||
# Remove old script folder if it exists, but keep configs, logs and backups
|
||||
if (Test-Path $tempWorkPath) {
|
||||
Write-Output ""
|
||||
Write-Output "> Cleaning up old Win11Debloat folder..."
|
||||
Write-Output "> Cleaning up old script files..."
|
||||
|
||||
Get-ChildItem -Path $tempWorkPath -Exclude Config,Logs,Backups | Remove-Item -Recurse -Force
|
||||
}
|
||||
@@ -184,8 +192,8 @@ if (Test-Path "$backupDir") {
|
||||
Remove-Item "$backupDir" -Recurse -Force
|
||||
}
|
||||
|
||||
# Make list of arguments to pass on to the script
|
||||
$arguments = $($PSBoundParameters.GetEnumerator() | ForEach-Object {
|
||||
# Make list of arguments to pass on to the script (exclude the -Dev switch, which only affects this launcher)
|
||||
$arguments = $($PSBoundParameters.GetEnumerator() | Where-Object { $_.Key -ne 'Dev' } | ForEach-Object {
|
||||
if ($_.Value -eq $true) {
|
||||
"-$($_.Key)"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Resolves display labels for selected features that require reboot.
|
||||
|
||||
.DESCRIPTION
|
||||
Combines parameter keys from both forward and undo selections, removes duplicates,
|
||||
and returns the feature label that should be shown to users. Undo selections use
|
||||
UndoLabel when available.
|
||||
#>
|
||||
function Get-RebootFeatureLabels {
|
||||
$rebootFeatureLabels = [System.Collections.Generic.List[string]]::new()
|
||||
$candidateParamKeys = (@($script:Params.Keys) + @($script:UndoParams.Keys)) | Select-Object -Unique
|
||||
|
||||
foreach ($paramKey in $candidateParamKeys) {
|
||||
if ($script:Features.ContainsKey($paramKey) -and $script:Features[$paramKey].RequiresReboot -eq $true) {
|
||||
$feature = $script:Features[$paramKey]
|
||||
$isUndo = $script:UndoParams.ContainsKey($paramKey)
|
||||
$displayLabel = if ($isUndo -and $feature.UndoLabel) { $feature.UndoLabel } else { $feature.Label }
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace([string]$displayLabel)) {
|
||||
[void]$rebootFeatureLabels.Add([string]$displayLabel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $rebootFeatureLabels
|
||||
}
|
||||
@@ -1,3 +1,17 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Normalize a user-name string for lookup and comparison.
|
||||
|
||||
.DESCRIPTION
|
||||
Strips zero-width chars and collapses whitespace so cosmetic input
|
||||
differences don't break downstream lookups. Returns '' for blank input.
|
||||
|
||||
.PARAMETER Value
|
||||
Raw user-supplied name to normalize.
|
||||
|
||||
.OUTPUTS
|
||||
System.String
|
||||
#>
|
||||
function NormalizeUserLookupValue {
|
||||
param(
|
||||
[string]$Value
|
||||
@@ -7,7 +21,6 @@ function NormalizeUserLookupValue {
|
||||
return ''
|
||||
}
|
||||
|
||||
# Remove zero-width characters and normalize whitespace for robust comparisons.
|
||||
$normalized = $Value -replace '[\u200B-\u200D\uFEFF]', ''
|
||||
$normalized = $normalized.Trim() -replace '\s+', ' '
|
||||
return $normalized
|
||||
@@ -17,6 +30,20 @@ if (-not $script:ResolvedUserSidCache) {
|
||||
$script:ResolvedUserSidCache = @{}
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Build a form-agnostic cache key for a user name.
|
||||
|
||||
.DESCRIPTION
|
||||
Normalized + lower-cased so the same identity hits regardless of
|
||||
case, whitespace, or qualifier form. Returns '' for blank input.
|
||||
|
||||
.PARAMETER Value
|
||||
User name to derive a key from.
|
||||
|
||||
.OUTPUTS
|
||||
System.String
|
||||
#>
|
||||
function GetUserLookupCacheKey {
|
||||
param(
|
||||
[string]$Value
|
||||
@@ -30,6 +57,49 @@ function GetUserLookupCacheKey {
|
||||
return $normalizedValue.ToLowerInvariant()
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Normalize and de-duplicate a set of user-name candidates.
|
||||
|
||||
.DESCRIPTION
|
||||
Centralizes the normalize/filter/dedupe step shared by the SID
|
||||
resolution fallbacks so the dedupe semantic lives in one place.
|
||||
Returns @() for empty or all-blank input.
|
||||
|
||||
.PARAMETER Candidates
|
||||
Equivalent name forms to normalize.
|
||||
|
||||
.OUTPUTS
|
||||
System.String[]
|
||||
#>
|
||||
function GetNormalizedLookupCandidates {
|
||||
param(
|
||||
[string[]]$Candidates
|
||||
)
|
||||
|
||||
$normalized = @($Candidates) |
|
||||
ForEach-Object { NormalizeUserLookupValue -Value $_ } |
|
||||
Where-Object { -not [string]::IsNullOrWhiteSpace($_) } |
|
||||
Select-Object -Unique
|
||||
|
||||
# The unary comma prevents PowerShell from unwrapping a single-element array.
|
||||
return ,@($normalized)
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Escape a string for safe embedding in a WQL single-quoted literal.
|
||||
|
||||
.DESCRIPTION
|
||||
Doubles embedded single quotes; without this a user name containing
|
||||
an apostrophe could break the WQL filter.
|
||||
|
||||
.PARAMETER Value
|
||||
String to escape.
|
||||
|
||||
.OUTPUTS
|
||||
System.String
|
||||
#>
|
||||
function EscapeWqlString {
|
||||
param(
|
||||
[string]$Value
|
||||
@@ -42,6 +112,20 @@ function EscapeWqlString {
|
||||
return $Value -replace "'", "''"
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Extract the local name segment from a possibly domain-qualified identity.
|
||||
|
||||
.DESCRIPTION
|
||||
Reduces DOMAIN\user or user@domain to the bare leaf, since profile
|
||||
folder leafs never carry the domain prefix. Returns '' for blank input.
|
||||
|
||||
.PARAMETER UserName
|
||||
User name that may be domain-qualified.
|
||||
|
||||
.OUTPUTS
|
||||
System.String
|
||||
#>
|
||||
function GetLocalUserNameSegment {
|
||||
param(
|
||||
[string]$UserName
|
||||
@@ -63,6 +147,322 @@ function GetLocalUserNameSegment {
|
||||
return $normalizedName
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Reduce a Win32_ComputerSystem.Domain value to a NetBIOS label.
|
||||
|
||||
.DESCRIPTION
|
||||
Win32_ComputerSystem.Domain may be DNS-style (e.g. contoso.com);
|
||||
prefer Win32_NTDomain.DomainName and fall back to the first DNS label
|
||||
so cached suffixes stay single-label (user.CONTOSO, not
|
||||
user.contoso.com). Returns the trimmed value as-is when already a
|
||||
single label. Falls back safely (no match) for FQDNs like
|
||||
corp.contoso.com whose NetBIOS label is CONTOSO.
|
||||
|
||||
.PARAMETER RawDomain
|
||||
Value reported by Win32_ComputerSystem.Domain.
|
||||
|
||||
.OUTPUTS
|
||||
System.String
|
||||
#>
|
||||
function ResolveNetBiosDomainName {
|
||||
param(
|
||||
[string]$RawDomain
|
||||
)
|
||||
|
||||
$trimmed = NormalizeUserLookupValue -Value $RawDomain
|
||||
if ([string]::IsNullOrWhiteSpace($trimmed)) {
|
||||
return ''
|
||||
}
|
||||
|
||||
try {
|
||||
# Prefer the joined-domain instance over the local SAM shadow
|
||||
# (DomainName == COMPUTERNAME); DomainControllerName may be $null.
|
||||
$computerName = $env:COMPUTERNAME
|
||||
$ntDomainInstances = @(Get-CimInstance -ClassName Win32_NTDomain -OperationTimeoutSec 5 -ErrorAction Stop |
|
||||
Where-Object {
|
||||
-not [string]::IsNullOrWhiteSpace($_.DomainName) -and
|
||||
$_.DomainName -ine 'WORKGROUP' -and
|
||||
$_.DomainName -ine $computerName
|
||||
})
|
||||
|
||||
$ntDomainInstance = $ntDomainInstances |
|
||||
Where-Object { -not [string]::IsNullOrWhiteSpace($_.DomainControllerName) } |
|
||||
Select-Object -First 1
|
||||
if (-not $ntDomainInstance -and $ntDomainInstances.Count -gt 0) {
|
||||
$ntDomainInstance = $ntDomainInstances | Select-Object -First 1
|
||||
}
|
||||
|
||||
if ($ntDomainInstance -and -not [string]::IsNullOrWhiteSpace($ntDomainInstance.DomainName)) {
|
||||
$fromNtDomain = NormalizeUserLookupValue -Value $ntDomainInstance.DomainName
|
||||
if (-not [string]::IsNullOrWhiteSpace($fromNtDomain)) {
|
||||
return $fromNtDomain
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
# Fall through to DNS-label derivation.
|
||||
}
|
||||
|
||||
if ($trimmed.Contains('.')) {
|
||||
$leaf = NormalizeUserLookupValue -Value (($trimmed -split '\.')[0])
|
||||
if (-not [string]::IsNullOrWhiteSpace($leaf)) {
|
||||
return $leaf
|
||||
}
|
||||
}
|
||||
|
||||
return $trimmed
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Determine whether the local machine is joined to a domain.
|
||||
|
||||
.DESCRIPTION
|
||||
Cached in script scope for the process lifetime. Returns $false on
|
||||
error or workgroup. When joined, also caches the NetBIOS domain label
|
||||
(ResolveNetBiosDomainName) for use as a profile-folder suffix.
|
||||
|
||||
.OUTPUTS
|
||||
System.Boolean
|
||||
#>
|
||||
function Test-MachineIsDomainJoined {
|
||||
if ($null -ne $script:MachineDomainJoinStateKnown) {
|
||||
return [bool]$script:MachineIsDomainJoined
|
||||
}
|
||||
|
||||
$script:MachineDomainJoinStateKnown = $true
|
||||
$script:MachineIsDomainJoined = $false
|
||||
$script:MachineNetBiosDomain = ''
|
||||
|
||||
try {
|
||||
$computerSystem = Get-CimInstance -ClassName Win32_ComputerSystem -ErrorAction Stop
|
||||
if ($null -ne $computerSystem -and $computerSystem.PartOfDomain) {
|
||||
$script:MachineIsDomainJoined = $true
|
||||
$script:MachineNetBiosDomain = ResolveNetBiosDomainName -RawDomain ([string]$computerSystem.Domain)
|
||||
}
|
||||
}
|
||||
catch {
|
||||
# Leave as $false; callers fall back to legacy matching.
|
||||
}
|
||||
|
||||
return [bool]$script:MachineIsDomainJoined
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Return the NetBIOS domain suffix Windows appends to profile folders.
|
||||
|
||||
.DESCRIPTION
|
||||
On domain-joined machines Windows writes profile folders as
|
||||
user.CONTOSO; knowing the suffix lets a bare name match that folder.
|
||||
Excluded on workgroup (USERDOMAIN == COMPUTERNAME) to avoid false
|
||||
matches. Returns '' when not applicable.
|
||||
|
||||
.OUTPUTS
|
||||
System.String
|
||||
#>
|
||||
function GetProfileFolderDomainSuffix {
|
||||
if (-not (Test-MachineIsDomainJoined)) {
|
||||
return ''
|
||||
}
|
||||
|
||||
$domain = $env:USERDOMAIN
|
||||
if ([string]::IsNullOrWhiteSpace($domain)) {
|
||||
# USERDOMAIN can be empty in restricted contexts; fall back to the
|
||||
# cached NetBIOS label (single-label, safe as a folder suffix).
|
||||
$domain = $script:MachineNetBiosDomain
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($domain)) {
|
||||
return ''
|
||||
}
|
||||
|
||||
# USERDOMAIN == COMPUTERNAME means effectively standalone; a suffix here
|
||||
# would produce false matches instead of disambiguation.
|
||||
if ($domain -ieq $env:COMPUTERNAME) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return $domain.Trim()
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Enumerate the name forms equivalent to a given identity.
|
||||
|
||||
.DESCRIPTION
|
||||
One identity surfaces in different forms (bare, qualified,
|
||||
domain-suffixed); enumerating equivalents lets cross-source matching
|
||||
succeed without broadening workgroup validation. Returns @() for blank.
|
||||
|
||||
.PARAMETER Value
|
||||
User name to expand into equivalent forms.
|
||||
|
||||
.OUTPUTS
|
||||
System.String[]
|
||||
#>
|
||||
function GetUserNameMatchCandidates {
|
||||
param(
|
||||
[string]$Value
|
||||
)
|
||||
|
||||
$normalized = NormalizeUserLookupValue -Value $Value
|
||||
if ([string]::IsNullOrWhiteSpace($normalized)) {
|
||||
return @()
|
||||
}
|
||||
|
||||
$candidates = New-Object 'System.Collections.Generic.List[string]'
|
||||
[void]$candidates.Add($normalized)
|
||||
|
||||
$localSegment = GetLocalUserNameSegment -UserName $normalized
|
||||
if (-not [string]::IsNullOrWhiteSpace($localSegment) -and ($localSegment -ine $normalized)) {
|
||||
[void]$candidates.Add($localSegment)
|
||||
}
|
||||
|
||||
# Domain-suffixed forms only apply where Windows writes them.
|
||||
$domainSuffix = GetProfileFolderDomainSuffix
|
||||
if (-not [string]::IsNullOrWhiteSpace($domainSuffix)) {
|
||||
# Prefer the local segment as the stem so DOMAIN\user still yields
|
||||
# user.CONTOSO rather than the fully qualified string.
|
||||
$stem = if (-not [string]::IsNullOrWhiteSpace($localSegment)) { $localSegment } else { $normalized }
|
||||
if (-not [string]::IsNullOrWhiteSpace($stem)) {
|
||||
$suffixedForm = "$stem.$domainSuffix"
|
||||
$alreadyPresent = $false
|
||||
foreach ($existing in $candidates) {
|
||||
if ($existing -ieq $suffixedForm) { $alreadyPresent = $true; break }
|
||||
}
|
||||
if (-not $alreadyPresent) {
|
||||
[void]$candidates.Add($suffixedForm)
|
||||
}
|
||||
}
|
||||
|
||||
# A suffixed input can also be referenced by its bare stem elsewhere
|
||||
# (registry, backup metadata), so add that form too.
|
||||
$suffixWithDot = ".$domainSuffix"
|
||||
if ($normalized.Length -gt $suffixWithDot.Length -and $normalized.EndsWith($suffixWithDot, [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||
$bareStem = NormalizeUserLookupValue -Value ($normalized.Substring(0, $normalized.Length - $suffixWithDot.Length))
|
||||
if (-not [string]::IsNullOrWhiteSpace($bareStem)) {
|
||||
$alreadyPresent = $false
|
||||
foreach ($existing in $candidates) {
|
||||
if ($existing -ieq $bareStem) { $alreadyPresent = $true; break }
|
||||
}
|
||||
if (-not $alreadyPresent) {
|
||||
[void]$candidates.Add($bareStem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $candidates.ToArray() | Select-Object -Unique
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Test whether a user name and a profile folder leaf share an account.
|
||||
|
||||
.DESCRIPTION
|
||||
Compares candidate sets (via GetUserNameMatchCandidates) instead of
|
||||
raw strings, so different forms of the same account still match.
|
||||
|
||||
.PARAMETER UserName
|
||||
User-supplied name to compare.
|
||||
|
||||
.PARAMETER ProfileLeaf
|
||||
On-disk profile folder leaf name to compare.
|
||||
|
||||
.OUTPUTS
|
||||
System.Boolean
|
||||
#>
|
||||
function Test-UserNameMatchesProfileLeaf {
|
||||
param(
|
||||
[string]$UserName,
|
||||
[string]$ProfileLeaf
|
||||
)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($UserName) -or [string]::IsNullOrWhiteSpace($ProfileLeaf)) {
|
||||
return $false
|
||||
}
|
||||
|
||||
$leafCandidates = @(GetUserNameMatchCandidates -Value $ProfileLeaf)
|
||||
$userCandidates = @(GetUserNameMatchCandidates -Value $UserName)
|
||||
|
||||
foreach ($leaf in $leafCandidates) {
|
||||
foreach ($user in $userCandidates) {
|
||||
if ($leaf -ieq $user) {
|
||||
return $true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $false
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Test whether two user-name strings refer to the same account.
|
||||
|
||||
.DESCRIPTION
|
||||
Accepts any equivalent candidate form so restore works across
|
||||
sessions and join states. Workgroup stays a strict normalized equality
|
||||
check to avoid broadening validation where suffixes aren't meaningful.
|
||||
|
||||
.PARAMETER UserNameA
|
||||
First user name to compare.
|
||||
|
||||
.PARAMETER UserNameB
|
||||
Second user name to compare.
|
||||
|
||||
.OUTPUTS
|
||||
System.Boolean
|
||||
#>
|
||||
function Test-UserNameMatch {
|
||||
param(
|
||||
[string]$UserNameA,
|
||||
[string]$UserNameB
|
||||
)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($UserNameA) -and [string]::IsNullOrWhiteSpace($UserNameB)) {
|
||||
return $true
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($UserNameA) -or [string]::IsNullOrWhiteSpace($UserNameB)) {
|
||||
return $false
|
||||
}
|
||||
|
||||
# Workgroup: strict equality (no suffix disambiguation available).
|
||||
if (-not (Test-MachineIsDomainJoined)) {
|
||||
$normalizedA = NormalizeUserLookupValue -Value $UserNameA
|
||||
$normalizedB = NormalizeUserLookupValue -Value $UserNameB
|
||||
return ($normalizedA -ieq $normalizedB)
|
||||
}
|
||||
|
||||
$candidatesA = @(GetUserNameMatchCandidates -Value $UserNameA)
|
||||
$candidatesB = @(GetUserNameMatchCandidates -Value $UserNameB)
|
||||
|
||||
foreach ($a in $candidatesA) {
|
||||
foreach ($b in $candidatesB) {
|
||||
if ($a -ieq $b) {
|
||||
return $true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $false
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Memoize a resolved SID under every equivalent name form.
|
||||
|
||||
.DESCRIPTION
|
||||
Keyed under all equivalent forms so later lookups short-circuit
|
||||
regardless of which variant the caller holds. No-op on blank SID.
|
||||
|
||||
.PARAMETER Candidates
|
||||
Equivalent name forms to key the cache entry under.
|
||||
|
||||
.PARAMETER Sid
|
||||
Resolved SID to cache.
|
||||
#>
|
||||
function SetResolvedUserSidCache {
|
||||
param(
|
||||
[string[]]$Candidates,
|
||||
@@ -81,6 +481,19 @@ function SetResolvedUserSidCache {
|
||||
}
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Retrieve a previously cached resolved SID.
|
||||
|
||||
.DESCRIPTION
|
||||
Probes every equivalent form; returns $null on a miss.
|
||||
|
||||
.PARAMETER Candidates
|
||||
Equivalent name forms to probe the cache with.
|
||||
|
||||
.OUTPUTS
|
||||
System.String
|
||||
#>
|
||||
function GetCachedResolvedUserSid {
|
||||
param(
|
||||
[string[]]$Candidates
|
||||
@@ -96,6 +509,20 @@ function GetCachedResolvedUserSid {
|
||||
return $null
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Attempt SID resolution via NTAccount translation.
|
||||
|
||||
.DESCRIPTION
|
||||
Most authoritative name->SID source; only resolves names reachable
|
||||
through standard security APIs. Returns $null on failure.
|
||||
|
||||
.PARAMETER UserName
|
||||
Name to translate.
|
||||
|
||||
.OUTPUTS
|
||||
System.String
|
||||
#>
|
||||
function TryResolveSidByNtAccount {
|
||||
param(
|
||||
[string]$UserName
|
||||
@@ -119,12 +546,26 @@ function TryResolveSidByNtAccount {
|
||||
return $null
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Attempt SID resolution against the local account database.
|
||||
|
||||
.DESCRIPTION
|
||||
Prefers Get-LocalUser (typed, fast), falls back to Win32_UserAccount
|
||||
CIM for older hosts or unavailable cmdlet. Returns $null if no match.
|
||||
|
||||
.PARAMETER Candidates
|
||||
Equivalent name forms to try.
|
||||
|
||||
.OUTPUTS
|
||||
System.String
|
||||
#>
|
||||
function TryResolveSidByLocalLookup {
|
||||
param(
|
||||
[string[]]$Candidates
|
||||
)
|
||||
|
||||
$lookupCandidates = @($Candidates) | ForEach-Object { NormalizeUserLookupValue -Value $_ } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique
|
||||
$lookupCandidates = GetNormalizedLookupCandidates -Candidates $Candidates
|
||||
if ($lookupCandidates.Count -eq 0) {
|
||||
return $null
|
||||
}
|
||||
@@ -162,12 +603,26 @@ function TryResolveSidByLocalLookup {
|
||||
return $null
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Recover a SID from the ProfileList registry hive.
|
||||
|
||||
.DESCRIPTION
|
||||
Last-resort heuristic: matches the profile folder leaf to recover a
|
||||
SID when name-resolution APIs fail. Returns $null on failure.
|
||||
|
||||
.PARAMETER Candidates
|
||||
Equivalent name forms to match against profile folder leafs.
|
||||
|
||||
.OUTPUTS
|
||||
System.String
|
||||
#>
|
||||
function TryResolveSidFromProfileList {
|
||||
param(
|
||||
[string[]]$Candidates
|
||||
)
|
||||
|
||||
$lookupCandidates = @($Candidates) | ForEach-Object { NormalizeUserLookupValue -Value $_ } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique
|
||||
$lookupCandidates = GetNormalizedLookupCandidates -Candidates $Candidates
|
||||
if ($lookupCandidates.Count -eq 0) {
|
||||
return $null
|
||||
}
|
||||
@@ -183,7 +638,12 @@ function TryResolveSidFromProfileList {
|
||||
$leafName = NormalizeUserLookupValue -Value (Split-Path -Leaf $expandedPath)
|
||||
|
||||
foreach ($candidate in $lookupCandidates) {
|
||||
if ($leafName -ieq $candidate) {
|
||||
if (Test-MachineIsDomainJoined) {
|
||||
if (Test-UserNameMatchesProfileLeaf -UserName $candidate -ProfileLeaf $leafName) {
|
||||
return $sidKey.PSChildName
|
||||
}
|
||||
}
|
||||
elseif ($leafName -ieq $candidate) {
|
||||
return $sidKey.PSChildName
|
||||
}
|
||||
}
|
||||
@@ -200,6 +660,26 @@ function TryResolveSidFromProfileList {
|
||||
return $null
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Construct a resolved user context object.
|
||||
|
||||
.DESCRIPTION
|
||||
Bundles UserName/UserSid/ProfilePath so callers don't re-derive or
|
||||
thread three loose values.
|
||||
|
||||
.PARAMETER UserName
|
||||
Normalized user name.
|
||||
|
||||
.PARAMETER UserSid
|
||||
Resolved SID, if available.
|
||||
|
||||
.PARAMETER ProfilePath
|
||||
Resolved profile folder path.
|
||||
|
||||
.OUTPUTS
|
||||
System.Management.Automation.PSCustomObject
|
||||
#>
|
||||
function NewResolvedUserContext {
|
||||
param(
|
||||
[string]$UserName,
|
||||
@@ -214,6 +694,74 @@ function NewResolvedUserContext {
|
||||
}
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Return the qualified (DOMAIN\user) name of the current process when it matches the input.
|
||||
|
||||
.DESCRIPTION
|
||||
Used to qualify a bare name on domain-joined boxes; returns $null when
|
||||
the input doesn't match the current process identity.
|
||||
|
||||
.PARAMETER Candidate
|
||||
Bare user name to compare against the current process identity.
|
||||
|
||||
.OUTPUTS
|
||||
System.String
|
||||
#>
|
||||
function GetQualifiedProcessIdentityName {
|
||||
param(
|
||||
[string]$Candidate
|
||||
)
|
||||
|
||||
$normalizedCandidate = NormalizeUserLookupValue -Value $Candidate
|
||||
if ([string]::IsNullOrWhiteSpace($normalizedCandidate)) {
|
||||
return $null
|
||||
}
|
||||
|
||||
try {
|
||||
$currentIdentity = [System.Security.Principal.WindowsIdentity]::GetCurrent()
|
||||
if ($null -eq $currentIdentity) {
|
||||
return $null
|
||||
}
|
||||
|
||||
# Skip service/SYSTEM identities (no user profile).
|
||||
$currentSidString = [string]$currentIdentity.User.Value
|
||||
if ($currentSidString -in @('S-1-5-18', 'S-1-5-19', 'S-1-5-20')) {
|
||||
return $null
|
||||
}
|
||||
|
||||
$currentName = [string]$currentIdentity.Name
|
||||
if ([string]::IsNullOrWhiteSpace($currentName)) {
|
||||
return $null
|
||||
}
|
||||
|
||||
$currentLocalSegment = GetLocalUserNameSegment -UserName $currentName
|
||||
if (-not [string]::IsNullOrWhiteSpace($currentLocalSegment) -and $currentLocalSegment -ieq $normalizedCandidate) {
|
||||
return $currentName
|
||||
}
|
||||
}
|
||||
catch {
|
||||
# Fall through to name-based resolution.
|
||||
}
|
||||
|
||||
return $null
|
||||
}
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Resolve a user name to its SID.
|
||||
|
||||
.DESCRIPTION
|
||||
Always qualifies the input first (DOMAIN\user) and resolves that form;
|
||||
never guesses from a bare name on domain-joined boxes to avoid
|
||||
same-named local SAM shadowing.
|
||||
|
||||
.PARAMETER UserName
|
||||
User name to resolve. May be bare, DOMAIN\user, or user@domain.
|
||||
|
||||
.OUTPUTS
|
||||
System.String
|
||||
#>
|
||||
function ResolveUserSid {
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
@@ -232,20 +780,15 @@ function ResolveUserSid {
|
||||
$leafNameCandidates = @($localNameSegment)
|
||||
}
|
||||
|
||||
$cacheCandidates = if ($hasQualifiedIdentity) {
|
||||
# Unqualified inputs probe both the bare name and (for qualified inputs) the
|
||||
# local leaf segment; qualified inputs pin to the caller's full form only.
|
||||
$lookupCandidates = 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
|
||||
}
|
||||
@@ -253,69 +796,99 @@ function ResolveUserSid {
|
||||
@($candidateUserName)
|
||||
}
|
||||
|
||||
$cachedSid = GetCachedResolvedUserSid -Candidates $cacheCandidates
|
||||
$cachedSid = GetCachedResolvedUserSid -Candidates $lookupCandidates
|
||||
if ($cachedSid) {
|
||||
return $cachedSid
|
||||
}
|
||||
|
||||
# Resolve fully-qualified identities first to avoid accidentally matching a local leaf account.
|
||||
# Step 1: derive the qualified form(s) to resolve; never guess from a bare
|
||||
# name on domain-joined boxes (local SAM nameshare risk).
|
||||
$qualifiedNamesToTry = New-Object 'System.Collections.Generic.List[string]'
|
||||
|
||||
if ($hasQualifiedIdentity) {
|
||||
$resolvedSid = TryResolveSidByNtAccount -UserName $candidateUserName
|
||||
# Caller already qualified; honor verbatim.
|
||||
[void]$qualifiedNamesToTry.Add($candidateUserName)
|
||||
}
|
||||
elseif (Test-MachineIsDomainJoined) {
|
||||
# Prefer process identity (authoritative), then USERDOMAIN\input.
|
||||
$processQualifiedName = GetQualifiedProcessIdentityName -Candidate $candidateUserName
|
||||
if (-not [string]::IsNullOrWhiteSpace($processQualifiedName)) {
|
||||
[void]$qualifiedNamesToTry.Add($processQualifiedName)
|
||||
}
|
||||
|
||||
$domainSuffix = GetProfileFolderDomainSuffix
|
||||
if (-not [string]::IsNullOrWhiteSpace($domainSuffix)) {
|
||||
$domainQualifiedName = "$domainSuffix\$candidateUserName"
|
||||
if (-not ($qualifiedNamesToTry -contains $domainQualifiedName)) {
|
||||
[void]$qualifiedNamesToTry.Add($domainQualifiedName)
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
# Workgroup: bare name is unambiguous.
|
||||
[void]$qualifiedNamesToTry.Add($candidateUserName)
|
||||
}
|
||||
|
||||
# Step 2: resolve qualified form(s) via NTAccount.Translate.
|
||||
foreach ($qualifiedName in $qualifiedNamesToTry) {
|
||||
$resolvedSid = TryResolveSidByNtAccount -UserName $qualifiedName
|
||||
if ($resolvedSid) {
|
||||
SetResolvedUserSidCache -Candidates $cacheCandidates -Sid $resolvedSid
|
||||
$allCacheKeys = @($candidateUserName) + $qualifiedNamesToTry | Select-Object -Unique
|
||||
SetResolvedUserSidCache -Candidates $allCacheKeys -Sid $resolvedSid
|
||||
return $resolvedSid
|
||||
}
|
||||
}
|
||||
|
||||
$resolvedSid = TryResolveSidByLocalLookup -Candidates $localLookupCandidates
|
||||
# Step 3: local SAM fallback (workgroup only; skipped on domain to avoid
|
||||
# nameshare shadowing).
|
||||
if (-not (Test-MachineIsDomainJoined)) {
|
||||
$resolvedSid = TryResolveSidByLocalLookup -Candidates $lookupCandidates
|
||||
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
|
||||
$allCacheKeys = @($candidateUserName) + $qualifiedNamesToTry | Select-Object -Unique
|
||||
SetResolvedUserSidCache -Candidates $allCacheKeys -Sid $resolvedSid
|
||||
return $resolvedSid
|
||||
}
|
||||
}
|
||||
|
||||
# Step 4: ProfileList leaf heuristic (last resort; disambiguates by
|
||||
# on-disk folder name, suffix-aware on domain boxes).
|
||||
$resolvedSid = TryResolveSidFromProfileList -Candidates $profileHeuristicCandidates
|
||||
if ($resolvedSid) {
|
||||
SetResolvedUserSidCache -Candidates $cacheCandidates -Sid $resolvedSid
|
||||
$allCacheKeys = @($candidateUserName) + $qualifiedNamesToTry | Select-Object -Unique
|
||||
SetResolvedUserSidCache -Candidates $allCacheKeys -Sid $resolvedSid
|
||||
return $resolvedSid
|
||||
}
|
||||
|
||||
return $null
|
||||
}
|
||||
|
||||
function ResolveUserProfilePath {
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$UserName
|
||||
)
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Resolve a user name to a full profile context (name, SID, path).
|
||||
|
||||
$userContext = ResolveUserProfileContext -UserName $UserName
|
||||
if ($userContext) {
|
||||
return $userContext.ProfilePath
|
||||
}
|
||||
.DESCRIPTION
|
||||
SID-keyed registry data first (authoritative), then on-disk path
|
||||
probing so resolution still succeeds when the account or SID can't be
|
||||
looked up (deleted account, restricted context). Returns $null if not
|
||||
found.
|
||||
|
||||
return $null
|
||||
}
|
||||
.PARAMETER UserName
|
||||
User name whose profile context is required.
|
||||
|
||||
.OUTPUTS
|
||||
System.Management.Automation.PSCustomObject
|
||||
#>
|
||||
function ResolveUserProfileContext {
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$UserName
|
||||
)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($UserName)) {
|
||||
$candidateUserName = NormalizeUserLookupValue -Value $UserName
|
||||
if ([string]::IsNullOrWhiteSpace($candidateUserName)) {
|
||||
return $null
|
||||
}
|
||||
|
||||
$candidateUserName = NormalizeUserLookupValue -Value $UserName
|
||||
$rootPaths = @(
|
||||
(Join-Path $env:SystemDrive 'Users')
|
||||
(Split-Path -Path $env:USERPROFILE -Parent)
|
||||
@@ -372,10 +945,26 @@ function ResolveUserProfileContext {
|
||||
continue
|
||||
}
|
||||
|
||||
# Exact leaf match first (common case; avoids an unnecessary scan).
|
||||
$candidateUserPath = Join-Path $rootPath $candidateUserName
|
||||
if (Test-Path -LiteralPath $candidateUserPath -PathType Container) {
|
||||
return (NewResolvedUserContext -UserName $candidateUserName -UserSid $userSid -ProfilePath $candidateUserPath)
|
||||
}
|
||||
|
||||
# Only domain-joined boxes write suffixed folders; scanning workgroup
|
||||
# roots would risk matching the wrong account.
|
||||
if (Test-MachineIsDomainJoined) {
|
||||
try {
|
||||
foreach ($child in @(Get-ChildItem -LiteralPath $rootPath -Directory -ErrorAction SilentlyContinue)) {
|
||||
if (Test-UserNameMatchesProfileLeaf -UserName $candidateUserName -ProfileLeaf $child.Name) {
|
||||
return (NewResolvedUserContext -UserName $candidateUserName -UserSid $userSid -ProfilePath $child.FullName)
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
# Fall through to the next root path.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $null
|
||||
|
||||
@@ -15,7 +15,7 @@ function Test-TargetUserName {
|
||||
}
|
||||
}
|
||||
|
||||
if ($normalizedUserName -eq $env:USERNAME) {
|
||||
if (Test-UserNameMatch -UserNameA $normalizedUserName -UserNameB $env:USERNAME) {
|
||||
return [PSCustomObject]@{
|
||||
IsValid = $false
|
||||
UserName = $normalizedUserName
|
||||
|
||||
+31
-4
@@ -30,7 +30,9 @@ param (
|
||||
[switch]$DisableUpdateASAP,
|
||||
[switch]$PreventUpdateAutoReboot,
|
||||
[switch]$DisableDeliveryOptimization,
|
||||
[switch]$DisableDeviceAutoAppDownload,
|
||||
[switch]$DisableBing,
|
||||
[switch]$DisableNotifications,
|
||||
[switch]$DisableStoreSearchSuggestions,
|
||||
[switch]$DisableSearchHighlights,
|
||||
[switch]$DisableDesktopSpotlight,
|
||||
@@ -99,6 +101,17 @@ param (
|
||||
[switch]$HideDriveLetters
|
||||
)
|
||||
|
||||
# Win11Debloat depends on Windows PowerShell 5.1 cmdlets (the Appx module's Get-AppxPackage /
|
||||
# Remove-AppxPackage, and Get-ComputerRestorePoint) that do not load in PowerShell 7 (pwsh), where the
|
||||
# Appx module fails with "Operation is not supported on this platform" (0x80131539). Without this guard
|
||||
# the run continues and silently fails to remove any apps while still reporting success. See issue #675.
|
||||
if ($PSVersionTable.PSEdition -eq 'Core') {
|
||||
Write-Host "Win11Debloat requires Windows PowerShell 5.1, but it is running under PowerShell $($PSVersionTable.PSVersion) (pwsh / Core edition)." -ForegroundColor Red
|
||||
Write-Host "App removal and system restore points rely on modules that are not available in PowerShell 7, so the run cannot complete correctly here." -ForegroundColor Red
|
||||
Write-Host "Please re-run this script with Windows PowerShell instead (powershell.exe)." -ForegroundColor Yellow
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Check if script is running as administrator
|
||||
$isAdmin = ([Security.Principal.WindowsPrincipal] `
|
||||
[Security.Principal.WindowsIdentity]::GetCurrent()
|
||||
@@ -111,7 +124,14 @@ if (-not $isAdmin) {
|
||||
$choice = Read-Host "Restart as Administrator? (y/n)"
|
||||
|
||||
if ($choice -match '^[Yy]$') {
|
||||
$elevatedArgs = @("-NoProfile", "-ExecutionPolicy", "Bypass", "-File", $PSCommandPath)
|
||||
# Win32-safe escaping for arguments to pass to elevated process
|
||||
function Format-ElevatedArg([string]$Value) {
|
||||
$escaped = $Value -replace '(\\*)"', '$1$1\"'
|
||||
$escaped = $escaped -replace '(\\+)$', '$1$1'
|
||||
return '"' + $escaped + '"'
|
||||
}
|
||||
|
||||
$elevatedArgs = @("-NoProfile", "-ExecutionPolicy", "Bypass", "-File", (Format-ElevatedArg $PSCommandPath))
|
||||
|
||||
foreach ($paramName in $PSBoundParameters.Keys) {
|
||||
$paramValue = $PSBoundParameters[$paramName]
|
||||
@@ -123,12 +143,14 @@ if (-not $isAdmin) {
|
||||
}
|
||||
else {
|
||||
$elevatedArgs += "-$paramName"
|
||||
$elevatedArgs += "$paramValue"
|
||||
$elevatedArgs += (Format-ElevatedArg $paramValue)
|
||||
}
|
||||
}
|
||||
|
||||
if ($MyInvocation.UnboundArguments.Count -gt 0) {
|
||||
$elevatedArgs += $MyInvocation.UnboundArguments
|
||||
foreach ($unboundArg in $MyInvocation.UnboundArguments) {
|
||||
$elevatedArgs += (Format-ElevatedArg "$unboundArg")
|
||||
}
|
||||
}
|
||||
|
||||
Start-Process powershell -ArgumentList $elevatedArgs -Verb RunAs
|
||||
@@ -137,7 +159,7 @@ if (-not $isAdmin) {
|
||||
}
|
||||
|
||||
# Define script-level variables & paths
|
||||
$script:Version = "2026.06.24"
|
||||
$script:Version = "2026.07.11"
|
||||
$configPath = Join-Path $PSScriptRoot 'Config'
|
||||
$logsPath = Join-Path $PSScriptRoot 'Logs'
|
||||
$schemasPath = Join-Path $PSScriptRoot 'Schemas'
|
||||
@@ -360,6 +382,7 @@ if (-not $script:WingetInstalled -and -not $Silent) {
|
||||
. "$PSScriptRoot/Scripts/Helpers/GenerateAppsList.ps1"
|
||||
. "$PSScriptRoot/Scripts/Helpers/GetFriendlyRegistryBackupTarget.ps1"
|
||||
. "$PSScriptRoot/Scripts/Helpers/GetFriendlyTargetUserName.ps1"
|
||||
. "$PSScriptRoot/Scripts/Helpers/Get-RebootFeatureLabels.ps1"
|
||||
. "$PSScriptRoot/Scripts/Helpers/ImportConfigToParams.ps1"
|
||||
. "$PSScriptRoot/Scripts/Helpers/GetTargetUserForAppRemoval.ps1"
|
||||
. "$PSScriptRoot/Scripts/Helpers/Get-RegFileOperations.ps1"
|
||||
@@ -540,7 +563,11 @@ if (($controlParamsCount -eq $script:Params.Keys.Count) -or ($script:Params.Keys
|
||||
# (This also handles restore point creation if requested)
|
||||
Invoke-AllChanges
|
||||
|
||||
|
||||
# Restart Explorer process unless running in Sysprep or User context
|
||||
if (-not ($script:Params.ContainsKey("Sysprep") -or $script:Params.ContainsKey("User"))) {
|
||||
RestartExplorer
|
||||
}
|
||||
|
||||
Write-Output ""
|
||||
Write-Output ""
|
||||
|
||||
Reference in New Issue
Block a user