Compare commits

...
Author SHA1 Message Date
Jeffrey 20d206cb9b fix: ensure CommandNotFoundException is not handled as task not found 2026-08-23 00:34:52 +02:00
Jeffrey b7f612f36e fix: improve error handling for registry file imports and telemetry scheduled tasks 2026-08-22 19:29:54 +02:00
Jeffrey c921730703 Enhance output documentation and error handling 2026-08-22 17:14:09 +02:00
Jeffrey 80eadd531c fix: add logging for unrecognized app-removal scope items 2026-08-16 22:46:26 +02:00
Jeffrey 4c29fce469 feat: improve consistency checks and error reporting 2026-08-16 22:04:00 +02:00
Jeffrey 93d77d8034 fix: remove redundant Category field from UiControlMappings in dynamic tweak controls 2026-08-16 21:27:58 +02:00
Jeffrey 78e1d601b0 fix: make intent of scopeTarget check more clear 2026-08-16 21:27:40 +02:00
Jeffrey f763390d53 fix: remove redundant Category field from UiControlMappings in dynamic tweak controls 2026-08-16 21:27:13 +02:00
Jeffrey dded2da3a7 Clean up & simplify error handling and reporting 2026-08-16 21:21:10 +02:00
Jeffrey 492a374f5c Refactor feature management scripts to improve error handling 2026-08-16 21:21:05 +02:00
JeffreyandGitHub 1a26934499 Improve error handling in Set-StoreSearchSuggestionsDisabled (#739) 2026-08-16 18:49:15 +02:00
SashankandGitHub 5072958b10 fix: match tweak presets and app-removal scope by stable ID instead of translatable text (#737) 2026-08-16 17:52:58 +02:00
JeffreyandGitHub 31feaeb6f5 Fix ForceRemoveEdge switch (#736) 2026-08-12 21:10:47 +02:00
Jeffrey dd929f8eec Surface errors when writing to file 2026-08-12 17:31:50 +02:00
Jeffrey b21be418cd fix: force removal of empty LastUsedSettings.json file 2026-08-12 16:40:26 +02:00
Jeffrey 25fb9f3725 Improve GUI error messaging 2026-08-12 16:38:14 +02:00
JeffreyandGitHub d1338cd027 Enhance app removal verification and error reporting (#735) 2026-08-12 15:56:29 +02:00
Jeffrey 9505a5a374 Update README ExecutionPolicy override instructions 2026-08-12 12:14:26 +02:00
JeffreyandGitHub c0599cba1f feat: add timeout for WinGet uninstall to prevent hanging (#731) 2026-08-09 01:24:27 +02:00
5c838384d6 fix: remove Mark-of-the-Web from script files at startup (#724)
Co-authored-by: Jeffrey <9938813+Raphire@users.noreply.github.com>
2026-08-09 01:24:14 +02:00
Jeffrey 6957ce4220 Update GUI logo 2026-08-01 16:16:24 +02:00
SashankandGitHub 0f30b62221 fix: support QWord, ExpandString and MultiString in the .reg fallback writer (#715) 2026-07-26 22:33:28 +02:00
JeffreyandGitHub 32cedaf65d Improve registry backup safety and add optional backup skipping (#710) 2026-07-25 20:05:49 +02:00
JeffreyandGitHub 68cacfce89 fix: force stop widget processes to prevent prompt (#713) 2026-07-23 22:23:00 +02:00
JeffreyandGitHub a9c1736e46 refactor: rename NoRestartExplorer parameter to SkipExplorerRestart (#709)
The old `NoRestartExplorer` parameter is kept as an alias and will
continue to work, but is considered deprecated
2026-07-19 22:58:03 +02:00
JeffreyandGitHub 9c033dbf98 Add comprehensive test suite, fix minor issues, rename function and file names to match approved verbs (#708) 2026-07-19 22:06:07 +02:00
Hill PatelandGitHub a7292e4f35 fix(registry-restore): reject Data/Kind mismatches before restore begins (#704) 2026-07-19 01:53:04 +02:00
KushidaandGitHub f739a0e474 fix: escape Windows Terminal path (#702) 2026-07-18 15:16:39 +02:00
JeffreyandGitHub 63b1f61fd1 Update README.md 2026-07-17 12:45:45 +02:00
de817399e6 fix: honor cancellation during changes (#700)
Co-authored-by: Jeffrey <9938813+Raphire@users.noreply.github.com>
2026-07-14 22:06:40 +02:00
Jeffrey 8fa1332ff0 Update download messages for development and stable versions of Win11Debloat 2026-07-14 21:18:39 +02:00
Jeffrey ce36b8edfb Bump version 2026-07-11 20:07:47 +02:00
JeffreyandGitHub 3b78e8d4ed Fix: Show reboot warning for undoing features that require it (#699) 2026-07-11 20:04:51 +02:00
Jeffrey 74dedc00e9 Add tooltip to user selection selectbox 2026-07-11 00:04:08 +02:00
Jeffrey 5571700634 Disabling Window Snapping now warns users that a reboot is required to apply 2026-07-09 22:02:18 +02:00
Jeffrey c2ea697552 Hide irrelevant Settings home page settings in Windows 10 2026-07-09 22:01:33 +02:00
JeffreyandGitHub ce8c10042e Fix issues with domain accounts & fix run.bat breaking when launched from path with certain characters (#693) 2026-07-08 23:18:04 +02:00
JeffreyandGitHub 89a5d508d8 Fix win10 icons by falling back to Segoe MDL2 Assets (#696) 2026-07-08 23:16:01 +02:00
Jeffrey e38aafe01e Update README.md 2026-07-08 21:40:33 +02:00
Jeffrey d336f98b39 Update feature wording 2026-07-08 21:38:48 +02:00
Raphire a70a2ccbd6 Update README.md 2026-07-08 19:30:25 +02:00
JeffreyandGitHub 0ef789cc3a Add option to prevent auto install of device apps & add LG monitor app removal (#695)
* Add support for removing LG monitor app

* Add option to prevent auto install of device companion apps via Windows Update

* Update CONTRIBUTING.md with missing Apps.json fields
2026-07-08 17:54:13 +02:00
HetCreepandGitHub 82f50d6d13 fix(startup): refuse to run under PowerShell 7 with a clear message (#676) 2026-07-08 15:22:36 +02:00
SqidyandGitHub 949515da3e Added Feature Disabling Notifications Through Win11Debloat (#690) 2026-07-06 19:29:09 +02:00
0eeb8f323a fix(elevation): quote script path and parameter values in admin relaunch (#683)
Co-authored-by: Jeffrey <9938813+Raphire@users.noreply.github.com>
2026-07-04 17:35:24 +02:00
HetCreepandGitHub a9a89c1f13 fix(store-search): correct deny permission removal in undo path (#684) 2026-07-04 13:57:50 +02:00
HetCreepandGitHub 339764eb18 Fix broken contributing link in README.md (#688) 2026-07-02 23:03:44 +02:00
3b4a5f16ad fix(restore-gui): surface registry-backup load failures to the user (#685)
Co-authored-by: Jeffrey <9938813+Raphire@users.noreply.github.com>
2026-07-02 21:16:47 +02:00
JeffreyandGitHub 9a700afaed Consolidate Get-Dev.ps1 into Get.ps1, add WhatIf support to Get.ps1 (#681) 2026-06-28 22:53:27 +02:00
Jeffrey 26b313372b Update README.md 2026-06-25 16:41:32 +02:00
Jeffrey 9ee0126259 Bump version 2026-06-24 22:13:48 +02:00
Jeffrey e23ecf36d6 Update minimum window sizing 2026-06-24 22:00:44 +02:00
JeffreyandGitHub 32dc3d6bdf Fix maximized window sizing (#673) 2026-06-24 21:45:53 +02:00
Jeffrey f76adc5054 Add docstrings 2026-06-24 20:55:17 +02:00
JeffreyandGitHub 95b583606d Update start menu backup/restore with timestamped filenames (#672) 2026-06-24 17:32:45 +02:00
JeffreyandGitHub 693b805114 Simplify Window management (#671) 2026-06-24 14:40:48 +02:00
JeffreyandGitHub 5ebc50d36a Guard against loading, saving & executing undefined features (#665) 2026-06-23 00:41:33 +02:00
JeffreyandGitHub d1fe541b62 Refactor: Cleanup app removal, remove legacy app list generator and CustomAppsList file support (#662)
* remove support for uninstalling old sunset apps

* Add color legend on app removal screen

* Remove legacy app list generator and custom apps file support
Replaced by GUI config export/import, dynamic RemovalMethod, and
CLI app removal settings saved to LastUsedSettings.json.

* Verify app removal by checking actual installation state instead of trusting winget output
2026-06-22 22:13:01 +02:00
JeffreyandGitHub 71e3f2e44d Refactor: ExecuteChanges to InvokeChanges, clean up for readability (#641) 2026-06-22 21:43:53 +02:00
HetCreepandGitHub 4891aa401a fix(app-removal): detect installed OneDrive in the "Only show installed" filter (#656) 2026-06-22 15:10:53 +02:00
HetCreepandGitHub a6d59c0dc1 fix(app-removal): detect WinGet uninstall failures by exit code, not English text (#658) 2026-06-22 14:53:45 +02:00
JeffreyandGitHub ac54bde383 Replace FocusHelper C# compilation with WPF Activate() to fix temp DLL access-denied errors (#661) 2026-06-22 02:07:26 +02:00
dfe7810346 feat(registry): add GPO override warning and WhatIf dry-run previews (#611)
Co-authored-by: Jeffrey <9938813+Raphire@users.noreply.github.com>
2026-06-21 21:30:31 +02:00
HetCreepandGitHub 82894176d9 fix(restore): correct sub-key path matching in backup allow-list validation (#645) 2026-06-21 20:37:29 +02:00
HetCreepandGitHub 87b3035eda fix(threading): surface runspace errors instead of swallowing them in GUI mode (#655) 2026-06-21 19:08:21 +02:00
JeffreyandGitHub a89b53504c Clean up styling to better match Windows fluent design guidelines (#638) 2026-06-21 18:47:52 +02:00
HetCreepandGitHub 91a6266d50 fix(gui): treat dismissed unsafe-removal confirmation as decline (#651) 2026-06-21 18:42:47 +02:00
HetCreepandGitHub 469751f8e8 fix(app-removal): don't treat AllUsers/CurrentUser as a username at startup (#647) 2026-06-21 18:39:10 +02:00
JeffreyandGitHub 908274a500 Fix store suggestions not getting disabled correctly for all users when running as other user (#642) 2026-06-21 01:56:28 +02:00
6e4a616f1c feat(telemetry): disable telemetry-related scheduled tasks under Microsoft\Windows (#615)
Co-authored-by: Jeffrey <9938813+Raphire@users.noreply.github.com>
2026-06-20 19:04:46 +02:00
JeffreyandGitHub 535b62db40 Fix: Respect Feature min/max version for comboboxes (#639) 2026-06-19 18:20:25 +02:00
JeffreyandGitHub c039b04717 Fix Start Menu apps not being set correctly for all users when running script for other user (#637) 2026-06-18 23:02:25 +02:00
Jeffrey a7a46bb5bf Clean up app removal target user descriptions 2026-06-16 23:43:07 +02:00
Jeffrey a95b5adee8 Update import/export window descriptions 2026-06-16 19:47:48 +02:00
Jeffrey 2b97021341 Merge branch 'master' of https://github.com/Raphire/Win11Debloat 2026-06-14 22:06:22 +02:00
Jeffrey 1235306f80 Bump version 2026-06-14 22:06:16 +02:00
JeffreyandGitHub 1a69d19f30 Refactor Get-RegFileOperations.ps1 (#626)
Feels weird to have to do this, but I have refactored the functions in Get-RegFileOperations.ps1 to avoid false positives in Windows Security (Windows Defender) and Bitdefender.

Related issues: #621, #624
2026-06-14 22:05:19 +02:00
Jeffrey 5628f6e0b7 Add logging around winget app retrieval and increase timeout to 20s 2026-06-12 17:41:30 +02:00
Jeffrey 6f349b4992 Update bug_report template 2026-06-12 17:34:17 +02:00
Jeffrey 2193591448 Bump version 2026-06-11 22:16:45 +02:00
JeffreyandGitHub e9269c5501 Fix lockscreen spotlight option being locked when start recommended section is disabled (#619) 2026-06-11 22:09:06 +02:00
Jeffrey fdac0a6d14 Move trailing ellipsis out of config 2026-06-10 21:00:07 +02:00
Jeffrey 2aa9afaa2c Update CONTRIBUTING.md 2026-06-10 20:55:26 +02:00
176 changed files with 12822 additions and 3849 deletions
+93 -21
View File
@@ -1,6 +1,7 @@
# How to Contribute?
We welcome contributions from the community. You can contribute to Win11Debloat by:
- Reporting issues and bugs [here](https://github.com/Raphire/Win11Debloat/issues/new?template=bug_report.yml)
- Submitting feature requests [here](https://github.com/Raphire/Win11Debloat/issues/new?template=feature_request.yml)
- Testing Win11Debloat
@@ -15,8 +16,9 @@ You can help us test the latest changes and additions to the script. If you enco
> The prerelease version of Win11Debloat is meant for developers to test the script. Don't use this in production environments!
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
@@ -28,12 +30,14 @@ You can launch the prerelease version of Win11Debloat by running this command:
1. **Fork the project** on GitHub by clicking the "Fork" button at the top right of the repository page.
2. **Clone the repository** to your local machine:
```powershell
git clone https://github.com/YOUR-USERNAME/Win11Debloat.git
cd Win11Debloat
```
3. **Create a new branch** for your contribution:
```powershell
git checkout -b feature/your-feature-name
```
@@ -42,33 +46,68 @@ You can launch the prerelease version of Win11Debloat by running this command:
1. Open PowerShell as an administrator
2. Enable script execution if necessary:
```powershell
Set-ExecutionPolicy Unrestricted -Scope Process -Force
```
3. Navigate to your Win11Debloat directory
4. Run the script:
```powershell
.\Win11Debloat.ps1
```
### Running Automated Tests
The automated test cases use Pester 5 and do not modify the registry or other
system state. The optional bootstrap step installs Pester for your user account
when needed. To run the complete suite:
```powershell
.\Scripts\Run-Tests.ps1 -Bootstrap
```
After the initial setup, run the suite with:
```powershell
.\Scripts\Run-Tests.ps1
```
GitHub Actions runs the same test command with Windows PowerShell 5.1 for pull
requests and pushes to `master`.
## Implementation Guidelines
### Project Structure
Understanding the project structure is essential for contributing effectively:
```
```text
Win11Debloat/
├── Win11Debloat.ps1 # Main PowerShell script
├── Run.bat # Batch launcher for the quick launch method
├── Scripts/ # Additional PowerShell scripts and functions
── Get.ps1 # Script used for the quick launch method to automatically download and run Win11debloat
── Get.ps1 # Script used for the quick launch method to automatically download and run Win11debloat
│ ├── AppRemoval/ # App package removal logic
│ ├── CLI/ # Command-line interface helpers
│ ├── Features/ # Feature apply/undo logic (e.g. Invoke-Changes.ps1, Replace-StartMenu.ps1)
│ ├── FileIO/ # File input/output helpers
│ ├── GUI/ # GUI window definitions and logic
│ ├── Helpers/ # Shared helper functions
│ └── Threading/ # Threading utilities
├── Config/
│ ├── Apps.json # List of supported apps for removal
│ ├── DefaultSettings.json # Default configuration preset
│ ├── Features.json # All features with metadata
│ └── LastUsedSettings.json # Last used configuration (generated during use)
├── Regfiles/ # Registry files for all features
└── Schemas/ # XAML Schemas for GUI elements
│ ├── Undo/ # Registry files for reverting features
│ └── Sysprep/ # Registry files for Sysprep mode
├── Schemas/ # XAML Schemas for GUI elements
├── Assets/ # Static assets (icons, start menu templates)
├── Backups/ # Registry backups (generated during use)
└── Logs/ # Script logs (generated during use)
```
### Best Practices
@@ -109,9 +148,9 @@ Avoid these common mistakes when contributing:
Placing files in the wrong directory may cause the script to fail when trying to apply or undo changes.
6. **Not Testing Undo Functionality**: Always test that your undo registry file properly reverts all changes.
5. **Not Testing Undo Functionality**: Always test that your undo registry file properly reverts all changes.
7. **Not Testing User/Sysprep Functionality**: Always test that your feature works when applied to another user or to the Windows default user with Sysprep. Sysprep changes can be tested by creating new users after running the script.
6. **Not Testing User/Sysprep Functionality**: Always test that your feature works when applied to another user or to the Windows default user with Sysprep. Sysprep changes can be tested by creating new users after running the script.
7. **Missing Category**: Features without a `Category` field (set to `null`) won't appear in the GUI. This is intentional for command-line-only features, make sure this is what you want before submitting.
@@ -127,24 +166,43 @@ Avoid these common mistakes when contributing:
To add a new app that can be removed via Win11Debloat:
1. **Find the AppId**: To find the correct AppId for an app:
```powershell
Get-AppxPackage | Select-Object Name, PackageFullName
```
2. **Edit `Config/Apps.json`**: Add a new entry to the `"Apps"` array:
```json
{
"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`
- Set `SelectedByDefault` to `true` only for apps that are largely considered bloatware, otherwise set to `false`
- Provide a concise description explaining what the app does
- Use clear, user-friendly names for `FriendlyName`
- Set `SelectedByDefault` to `true` only for apps that are largely considered bloatware, otherwise set to `false`
- Provide a concise description explaining what the app does
### Adding a New Feature
@@ -162,6 +220,7 @@ Create new registry files in the `Regfiles/` directory:
- **Sysprep file**: `Sysprep/Disable_YourFeature.reg` (for Sysprep mode)
Example registry file structure:
```reg
Windows Registry Editor Version 5.00
@@ -170,6 +229,7 @@ Windows Registry Editor Version 5.00
```
A Sysprep registry file should apply the same changes as the normal action. Replace the hive of registry keys that start with `HKEY_CURRENT_USER` with `hkey_users\default`. For example:
```reg
Windows Registry Editor Version 5.00
@@ -179,7 +239,7 @@ Windows Registry Editor Version 5.00
#### 1b. Implement the Feature Logic
If your feature requires more than just applying a registry file, add custom logic to the main script in the appropriate section. In most cases this will involve creating a new entry in the `ExecuteParameter` function for your new feature.
If your feature requires more than just applying a registry file, add custom logic to the main script in the appropriate section. In most cases this will involve creating a new entry in the `Invoke-FeatureApply` function (in `Scripts/Features/Invoke-Changes.ps1`) for your new feature. If your feature also requires custom undo logic (beyond a simple registry file import), add a corresponding entry to the `Invoke-FeatureUndo` function in the same file.
#### 2. Add Feature to Features.json
@@ -192,18 +252,20 @@ Add your feature to the `"Features"` array in `Config/Features.json`:
"ToolTip": "Detailed explanation of what this feature does and its impact.",
"Category": "Privacy & Suggested Content",
"Priority": 1,
"Action": "Disable",
"RegistryKey": "Disable_YourFeature.reg",
"ApplyText": "Disabling your feature...",
"UndoAction": "Enable",
"ApplyText": "Disabling your feature",
"UndoLabel": "Short description for the undo",
"ApplyUndoText": "Enabling your feature",
"RegistryUndoKey": "Enable_YourFeature.reg",
"RequiresReboot": false,
"DisableWhenApplied": false,
"MinVersion": null,
"MaxVersion": null
}
```
**Field Descriptions**:
- `FeatureId`: Unique identifier, this must match parameter name in the Win11Debloat.ps1 and Get.ps1 files.
- `Label`: Short description shown in the UI and wiki documentation.
- `ToolTip`: Detailed explanation of what the feature does, used for tooltips in the GUI.
@@ -222,6 +284,7 @@ Add your feature to the `"Features"` array in `Config/Features.json`:
#### 3. Add Command-Line Parameter
Add a corresponding parameter to both `Win11Debloat.ps1` AND `Scripts/Get.ps1`, the parameter name should match the FeatureId you have defined in `Features.json`. In most cases this will be a switch parameter, example:
```powershell
[switch]$YourFeatureId,
```
@@ -234,12 +297,14 @@ Add a corresponding parameter to both `Win11Debloat.ps1` AND `Scripts/Get.ps1`,
The default preset (`Config/DefaultSettings.json`) defines which features are automatically applied when users run Win11Debloat in "Default Mode" or with the `-RunDefaults` parameter. This preset should include features that are widely considered to improve the Windows experience without breaking functionality.
**When to add a feature to the default preset:**
- The feature removes obvious bloatware or distractions
- The feature enhances privacy without breaking core functionality
- The feature is generally non-controversial and beneficial to most users
- The change can be easily reverted if needed
**When NOT to add a feature to the default preset:**
- The feature significantly changes core Windows behavior
- The feature might break applications or workflows for some users
- The feature is highly opinionated or preference-based
@@ -255,10 +320,12 @@ To add your feature to the default preset, edit `Config/DefaultSettings.json` an
```
**Field Descriptions**:
- `Name`: Must exactly match the `FeatureId` from Features.json
- `Value`: Set to `true` to enable the feature in default mode
**Example:**
```json
{
"Version": "1.0",
@@ -284,12 +351,13 @@ To add your feature to the default preset, edit `Config/DefaultSettings.json` an
To add a new category for organizing features:
- Add a new category entry to the `"Categories"` array in `Config/Features.json`:
```json
{
"Name": "Your Category Name",
"Icon": "&#xE#### ;"
}
```
```json
{
"Name": "Your Category Name",
"Icon": "&#xE#### ;"
}
```
> [!TIP]
> Use [Segoe Fluent Icon Assets](https://learn.microsoft.com/en-us/windows/apps/design/iconography/segoe-fluent-icons-font) for icon codes.
@@ -321,17 +389,20 @@ UI Groups allow features to be grouped together in the GUI with a combobox (drop
## Submitting a Pull Request
1. **Commit your changes** with clear, descriptive commit messages:
```powershell
git add .
git commit -m "Add feature: Description of your changes"
```
2. **Push to your fork**:
```powershell
git push origin feature/your-feature-name
```
3. **Create a Pull Request** on GitHub:
- Go to the original Win11Debloat repository
- Click "New Pull Request"
- Select your fork and branch
@@ -343,6 +414,7 @@ UI Groups allow features to be grouped together in the GUI with a combobox (drop
# Questions?
If you have questions about contributing, feel free to:
- Open a [discussion](https://github.com/Raphire/Win11Debloat/discussions)
- Comment on an existing issue
- Ask in your pull request
- Ask in your pull request
+1 -1
View File
@@ -1,6 +1,6 @@
name: "🐞 Bug report"
description: "Report an issue you encountered"
labels: ["bug"]
labels: ["bug", "unconfirmed"]
body:
- type: markdown
+26
View File
@@ -0,0 +1,26 @@
name: Tests
on:
pull_request:
push:
branches:
- master
workflow_dispatch:
permissions:
contents: read
jobs:
pester:
name: Pester (Windows PowerShell 5.1)
runs-on: windows-latest
timeout-minutes: 15
steps:
- name: Check out repository
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Run tests
shell: powershell
run: ./Scripts/Run-Tests.ps1 -Bootstrap
-4
View File
@@ -1,7 +1,3 @@
LastSettings
SavedSettings
LastUsedSettings.json
CustomAppsList
Logs/*
Win11Debloat.log
Backups/*
+303 -203
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -70,4 +70,4 @@
"Value": true
}
]
}
}
+219 -192
View File
File diff suppressed because it is too large Load Diff
+24 -43
View File
@@ -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.
![Win11Debloat Menu](/Assets/Images/menu.png)
@@ -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.
@@ -57,7 +57,7 @@ This method supports command-line parameters to customize the behaviour of the s
3. Temporarily enable PowerShell execution by entering the following command:
```PowerShell
Set-ExecutionPolicy Unrestricted -Scope Process -Force
Set-ExecutionPolicy Bypass -Scope Process -Force
```
4. In PowerShell, navigate to the directory where the files were extracted. Example: `cd c:\Win11Debloat`
@@ -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, like LG Monitor App, Alienware Command Center and more.
#### 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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+11 -11
View File
@@ -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:'=''%'; $w='%wtPath:'=''%'; $q=[char]34; Start-Process -FilePath $w -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
+17 -11
View File
@@ -35,7 +35,7 @@
</Grid.RowDefinitions>
<!-- Message Text -->
<Grid Grid.Row="0" Margin="20,4,20,20">
<Grid Grid.Row="0" Margin="20,4,20,16">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
@@ -79,15 +79,15 @@
<!-- Separator -->
<Border Grid.Row="1"
Height="1"
Background="{DynamicResource BorderColor}"
Background="{DynamicResource AppBorderColor}"
Margin="20,0"/>
<!-- Content -->
<StackPanel Grid.Row="2" Margin="20,18,20,20">
<StackPanel Grid.Row="2" Margin="20,16,20,16">
<!-- Donation Message -->
<TextBlock Text="Win11Debloat is a passion project that I maintain in my free time. If you've found this tool useful, please consider making a small donation to support its development. I really appreciate it!"
FontSize="14"
Foreground="{DynamicResource FgColor}"
Foreground="{DynamicResource AppFgColor}"
TextWrapping="Wrap"
Margin="0,0,0,15"/>
@@ -105,9 +105,9 @@
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontWeight="SemiBold"
FontFamily="Segoe Fluent Icons"
FontFamily="{DynamicResource AppIconFontFamily}"
Text="&#xEB52;"
Foreground="{DynamicResource CloseHover}"
Foreground="{DynamicResource TitleBarCloseHoverColor}"
Margin="0,0,8,0"/>
<TextBlock x:Name="KofiLink"
@@ -124,11 +124,17 @@
<!-- Button Panel -->
<Border Grid.Row="2" Style="{DynamicResource ModalFooterBorderStyle}">
<StackPanel x:Name="ButtonPanel" Style="{DynamicResource ModalFooterButtonsRightStyle}">
<Button x:Name="CloseButton"
Content="Close"
Style="{DynamicResource ModalSecondaryActionButtonStyle}"/>
</StackPanel>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Button x:Name="CloseButton"
Grid.Column="1"
Content="Close"
Style="{DynamicResource ModalSecondaryStretchedButtonStyle}"/>
</Grid>
</Border>
</Grid>
</Border>
+13 -13
View File
@@ -7,16 +7,16 @@
WindowStyle="None"
AllowsTransparency="True"
Background="Transparent"
Foreground="{DynamicResource FgColor}">
Foreground="{DynamicResource AppFgColor}">
<Window.Resources>
<!-- Title Bar Button Style -->
<Style x:Key="TitleBarButton" TargetType="Button">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
<Setter Property="Foreground" Value="{DynamicResource AppFgColor}"/>
<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>
@@ -31,17 +31,17 @@
<Style x:Key="CloseButton" TargetType="Button" BasedOn="{StaticResource TitleBarButton}">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="{DynamicResource CloseHover}"/>
<Setter Property="Background" Value="{DynamicResource TitleBarCloseHoverColor}"/>
<Setter Property="Foreground" Value="White"/>
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources>
<Border BorderBrush="{DynamicResource BorderColor}"
<Border BorderBrush="{DynamicResource AppBorderColor}"
BorderThickness="1"
CornerRadius="8"
Background="{DynamicResource BgColor}"
Background="{DynamicResource AppBgColor}"
Margin="25">
<Border.Effect>
<DropShadowEffect Color="Black"
@@ -58,7 +58,7 @@
<!-- Custom Title Bar -->
<Grid Grid.Row="0" x:Name="TitleBar">
<Border Background="{DynamicResource BgColor}" CornerRadius="8,8,0,0">
<Border Background="{DynamicResource AppBgColor}" CornerRadius="8,8,0,0">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
@@ -66,7 +66,7 @@
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0"
Text="Win11Debloat Application Selection"
Foreground="{DynamicResource FgColor}"
Foreground="{DynamicResource AppFgColor}"
VerticalAlignment="Center"
Margin="12,0,0,0"
FontSize="12"/>
@@ -90,27 +90,27 @@
<TextBlock Grid.Row="0"
Text="Check apps that you wish to remove, uncheck apps that you wish to keep"
TextWrapping="Wrap"
Foreground="{DynamicResource FgColor}"
Foreground="{DynamicResource AppFgColor}"
Margin="0,0,0,10"/>
<StackPanel Grid.Row="1" Orientation="Horizontal" Margin="0,0,0,8">
<CheckBox x:Name="CheckAllBox" Content="Check/Uncheck all" Margin="8,0,15,0" Foreground="{DynamicResource FgColor}" AutomationProperties.Name="Check or Uncheck all"/>
<CheckBox x:Name="CheckAllBox" Content="Check/Uncheck all" Margin="8,0,15,0" Foreground="{DynamicResource AppFgColor}" AutomationProperties.Name="Check or Uncheck all"/>
</StackPanel>
<Border Grid.Row="2" BorderBrush="{DynamicResource BorderColor}" CornerRadius="4" BorderThickness="1" Margin="0,0,0,10" Background="{DynamicResource CardBgColor}">
<Border Grid.Row="2" BorderBrush="{DynamicResource AppBorderColor}" CornerRadius="4" BorderThickness="1" Margin="0,0,0,10" Background="{DynamicResource CardBgColor}">
<Grid>
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel x:Name="AppsPanel" Margin="5"/>
</ScrollViewer>
<Border x:Name="LoadingAppsIndicator" CornerRadius="4" Background="{DynamicResource ScrollBarThumbColor}" Opacity="0.8" Visibility="Collapsed">
<TextBlock Text="Loading apps..." FontSize="16" FontWeight="SemiBold" Foreground="{DynamicResource FgColor}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
<TextBlock Text="Loading apps..." FontSize="16" FontWeight="SemiBold" Foreground="{DynamicResource AppFgColor}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</Grid>
</Border>
<StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Right">
<Grid Margin="0,8,10,10">
<CheckBox x:Name="OnlyInstalledBox" Content="Only show installed apps" Foreground="{DynamicResource FgColor}" AutomationProperties.Name="Only show installed apps"/>
<CheckBox x:Name="OnlyInstalledBox" Content="Only show installed apps" Foreground="{DynamicResource AppFgColor}" AutomationProperties.Name="Only show installed apps"/>
</Grid>
<Button x:Name="ConfirmBtn" Width="80" Height="32" Margin="0,0,10,0" Content="Confirm" Style="{DynamicResource PrimaryButtonStyle}" AutomationProperties.Name="Confirm"/>
<Button x:Name="CancelBtn" Width="80" Height="32" Content="Cancel" Style="{DynamicResource SecondaryButtonStyle}" IsCancel="True" AutomationProperties.Name="Cancel"/>
+12 -12
View File
@@ -36,15 +36,15 @@
<Button.Style>
<Style TargetType="Button">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
<Setter Property="Foreground" Value="{DynamicResource AppFgColor}"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="{DynamicResource CloseHover}"/>
<Setter Property="Background" Value="{DynamicResource TitleBarCloseHoverColor}"/>
</Trigger>
</Style.Triggers>
</Style>
</Button.Style>
<TextBlock Text="&#xE8BB;" FontFamily="Segoe Fluent Icons" FontSize="10"/>
<TextBlock Text="&#xE8BB;" FontFamily="{DynamicResource AppIconFontFamily}" FontSize="10"/>
</Button>
<StackPanel Grid.Row="0" Grid.RowSpan="2">
@@ -54,9 +54,9 @@
<!-- Loading icon (spinning) -->
<TextBlock x:Name="ApplySpinnerIcon"
Text="&#xE895;"
FontFamily="Segoe Fluent Icons"
FontFamily="{DynamicResource AppIconFontFamily}"
FontSize="36"
Foreground="{DynamicResource ButtonBg}"
Foreground="{DynamicResource ButtonBgColor}"
HorizontalAlignment="Center"
Margin="0,0,0,16"
RenderTransformOrigin="0.5,0.5">
@@ -102,7 +102,7 @@
<TextBlock x:Name="ApplyStepCounter"
Text="Step 0 of 0"
FontSize="12"
Foreground="{DynamicResource FgColor}"
Foreground="{DynamicResource AppFgColor}"
HorizontalAlignment="Right"
Opacity="0.8"/>
</StackPanel>
@@ -115,9 +115,9 @@
<!-- Success icon -->
<TextBlock x:Name="ApplyCompletionIcon"
Text="&#xE73E;"
FontFamily="Segoe Fluent Icons"
FontFamily="{DynamicResource AppIconFontFamily}"
FontSize="40"
Foreground="{DynamicResource ButtonBg}"
Foreground="{DynamicResource ButtonBgColor}"
HorizontalAlignment="Center"
Margin="0,0,0,12"/>
@@ -134,14 +134,14 @@
<!-- Reboot required section -->
<Border x:Name="ApplyRebootPanel"
Visibility="Collapsed"
BorderBrush="{DynamicResource BorderColor}"
BorderBrush="{DynamicResource AppBorderColor}"
HorizontalAlignment="Center"
BorderThickness="0,1,0,1"
Padding="24,12,24,14">
<StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,0,0,6">
<TextBlock Text="&#xE7BA;"
FontFamily="Segoe Fluent Icons"
FontFamily="{DynamicResource AppIconFontFamily}"
FontSize="14"
Foreground="#e8912d"
VerticalAlignment="Center"
@@ -149,7 +149,7 @@
<TextBlock Text="A reboot is required for these changes to take effect:"
FontSize="13"
FontWeight="SemiBold"
Foreground="{DynamicResource FgColor}"
Foreground="{DynamicResource AppFgColor}"
VerticalAlignment="Center"/>
</StackPanel>
<StackPanel x:Name="ApplyRebootList" Margin="22,0,0,0"/>
@@ -163,7 +163,7 @@
Style="{DynamicResource ModalSecondaryStretchedButtonStyle}"
AutomationProperties.Name="Support the creator">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="&#xEB52;" FontFamily="Segoe Fluent Icons" FontSize="14" VerticalAlignment="Center" Margin="0,0,8,-1"/>
<TextBlock Text="&#xEB52;" FontFamily="{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>
+1 -1
View File
@@ -26,7 +26,7 @@
Text="View the selected changes here"
TextWrapping="Wrap"
MaxWidth="260"
Foreground="{DynamicResource FgColor}"/>
Foreground="{DynamicResource AppFgColor}"/>
</Border>
</Grid>
+2 -2
View File
@@ -29,12 +29,12 @@
<!-- Content -->
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto" Padding="0,0,8,0">
<StackPanel x:Name="ContentPanel" Margin="20,4,20,8">
<StackPanel x:Name="ContentPanel" Margin="20,4">
<TextBlock x:Name="PromptText"
TextWrapping="Wrap"
FontSize="14"
LineHeight="20"
Foreground="{DynamicResource FgColor}"
Foreground="{DynamicResource AppFgColor}"
Margin="0,0,0,14"/>
<!-- Checkboxes are added dynamically at runtime -->
<StackPanel x:Name="CheckboxPanel"/>
+240 -256
View File
@@ -2,14 +2,14 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:shell="clr-namespace:System.Windows.Shell;assembly=PresentationFramework"
Title="Win11Debloat"
MinWidth="860" MinHeight="600"
MinWidth="860" MinHeight="640"
ResizeMode="CanResize"
SnapsToDevicePixels="True"
WindowStartupLocation="CenterScreen"
WindowStyle="None"
AllowsTransparency="False"
Background="{DynamicResource BgColor}"
Foreground="{DynamicResource FgColor}">
Background="{DynamicResource AppBorderColor}"
Foreground="{DynamicResource AppFgColor}">
<shell:WindowChrome.WindowChrome>
<shell:WindowChrome ResizeBorderThickness="5"
CaptionHeight="32"
@@ -30,8 +30,8 @@
</Style>
<!-- TextBlock Label Style -->
<Style x:Key="LabelStyle" TargetType="TextBlock">
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="Foreground" Value="{DynamicResource AppFgColor}"/>
<Setter Property="FontSize" Value="13"/>
<Setter Property="Margin" Value="0,0,0,2"/>
</Style>
@@ -46,28 +46,30 @@
<!-- Category card border style -->
<Style x:Key="CategoryCardBorderStyle" TargetType="Border">
<Setter Property="BorderBrush" Value="{DynamicResource BorderColor}"/>
<Setter Property="BorderBrush" Value="{DynamicResource AppBorderColor}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="CornerRadius" Value="4"/>
<Setter Property="Background" Value="{DynamicResource CardBgColor}"/>
<Setter Property="Padding" Value="16,12,16,2"/>
<Setter Property="Padding" Value="16,13,16,4"/>
<Setter Property="Margin" Value="0,0,0,16"/>
</Style>
<!-- Category header text style -->
<Style x:Key="CategoryHeaderTextBlock" TargetType="TextBlock">
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="FontSize" Value="16"/>
<Setter Property="Margin" Value="0,0,0,12"/>
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="FontSize" Value="18"/>
<Setter Property="LineHeight" Value="20"/>
<Setter Property="Margin" Value="0,0,0,16"/>
<Setter Property="Foreground" Value="{DynamicResource AppFgColor}"/>
</Style>
<!-- Category header icon style -->
<Style x:Key="CategoryHeaderIcon" TargetType="TextBlock">
<Setter Property="FontFamily" Value="Segoe Fluent Icons"/>
<Setter Property="FontSize" Value="19"/>
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
<Setter Property="Margin" Value="0,0,8,12"/>
<Setter Property="FontFamily" Value="{DynamicResource AppIconFontFamily}"/>
<Setter Property="FontSize" Value="20"/>
<Setter Property="LineHeight" Value="20"/>
<Setter Property="Foreground" Value="{DynamicResource AppFgColor}"/>
<Setter Property="Margin" Value="0,0,8,16"/>
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
@@ -83,7 +85,7 @@
<Setter Property="IsTabStop" Value="False"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
<Setter Property="Foreground" Value="{DynamicResource ButtonBg}"/>
<Setter Property="Foreground" Value="{DynamicResource ButtonBgColor}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
@@ -95,17 +97,18 @@
</Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Foreground" Value="{DynamicResource ButtonHover}"/>
<Setter Property="Foreground" Value="{DynamicResource ButtonHoverColor}"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Foreground" Value="{DynamicResource ButtonPressed}"/>
<Setter Property="Foreground" Value="{DynamicResource ButtonPressedColor}"/>
</Trigger>
</Style.Triggers>
</Style>
<Style x:Key="CategoryHelpLinkTextStyle" TargetType="TextBlock">
<Setter Property="FontSize" Value="12"/>
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Margin" Value="0,0,0,5"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="Foreground">
<Setter.Value>
@@ -116,14 +119,14 @@
<!-- Overview changes text style -->
<Style x:Key="OverviewNoChangesTextStyle" TargetType="TextBlock">
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
<Setter Property="Foreground" Value="{DynamicResource AppFgColor}"/>
<Setter Property="Margin" Value="0,0,0,8"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="FontStyle" Value="Italic"/>
</Style>
<Style x:Key="OverviewChangeBulletStyle" TargetType="TextBlock">
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
<Setter Property="Foreground" Value="{DynamicResource AppFgColor}"/>
<Setter Property="Margin" Value="0,0,0,8"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="TextWrapping" Value="Wrap"/>
@@ -131,45 +134,7 @@
<!-- TextBox Style for search -->
<Style TargetType="TextBox">
<Setter Property="CaretBrush" Value="{DynamicResource FgColor}"/>
</Style>
<!-- User TextBox Style with disabled state -->
<Style x:Key="UserTextBoxStyle" TargetType="TextBox">
<Setter Property="CaretBrush" Value="{DynamicResource FgColor}"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="FontSize" Value="13"/>
<Setter Property="Margin" Value="1,0,0,1"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="Padding" Value="0"/>
<Style.Triggers>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Background" Value="{DynamicResource ButtonDisabled}"/>
<Setter Property="Foreground" Value="{DynamicResource ButtonTextDisabled}"/>
<Setter Property="Opacity" Value="0.6"/>
</Trigger>
</Style.Triggers>
</Style>
<!-- User TextBox Border Style with disabled state -->
<Style x:Key="UserTextBoxBorderStyle" TargetType="Border">
<Setter Property="Background" Value="{DynamicResource ComboBgColor}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderColor}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="CornerRadius" Value="4"/>
<Setter Property="Padding" Value="8,6"/>
<Style.Triggers>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Background" Value="{DynamicResource ButtonDisabled}"/>
<Setter Property="BorderBrush" Value="{DynamicResource BorderColor}"/>
<Setter Property="Opacity" Value="0.6"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="{DynamicResource ComboHoverColor}"/>
</Trigger>
</Style.Triggers>
<Setter Property="CaretBrush" Value="{DynamicResource AppFgColor}"/>
</Style>
<!-- TextBlock style for App ID column in apps table -->
@@ -194,7 +159,7 @@
<!-- Column widths for the app table rows and header (dot | name | description | id) -->
<GridLength x:Key="AppTableDotColWidth">16</GridLength>
<GridLength x:Key="AppTableNameColWidth">151</GridLength>
<GridLength x:Key="AppTableNameColWidth">180</GridLength>
<GridLength x:Key="AppTableDescColWidth">1*</GridLength>
<GridLength x:Key="AppTableIdColWidth">261</GridLength>
@@ -219,14 +184,19 @@
<SolidColorBrush x:Key="ValidationErrorColor" Color="#c42b1c"/>
<SolidColorBrush x:Key="ValidationSuccessColor" Color="#28a745"/>
<!-- App recommendation dot / legend colors -->
<SolidColorBrush x:Key="AppRecommendationSafeColor" Color="#4CAF50"/>
<SolidColorBrush x:Key="AppRecommendationOptionalColor" Color="#FFC107"/>
<SolidColorBrush x:Key="AppRecommendationUnsafeColor" Color="#F44336"/>
<!-- Title Bar Button Style -->
<Style x:Key="TitleBarButton" TargetType="Button">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
<Setter Property="Foreground" Value="{DynamicResource AppFgColor}"/>
<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>
@@ -237,11 +207,21 @@
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<DataTrigger Binding="{Binding IsActive, RelativeSource={RelativeSource AncestorType=Window}}" Value="False">
<Setter Property="Foreground" Value="{DynamicResource TitleBarUnfocusedFgColor}"/>
</DataTrigger>
</Style.Triggers>
</Style>
<Style x:Key="CloseButton" TargetType="Button" BasedOn="{StaticResource TitleBarButton}">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="{DynamicResource CloseHover}"/>
<Setter Property="Background" Value="{DynamicResource TitleBarCloseHoverColor}"/>
<Setter Property="Foreground" Value="{DynamicResource AppFgColor}"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Background" Value="{DynamicResource TitleBarClosePressedColor}"/>
<Setter Property="Foreground" Value="{DynamicResource AppFgColor}"/>
</Trigger>
</Style.Triggers>
</Style>
@@ -257,25 +237,12 @@
</Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="{DynamicResource TitlebarButtonHover}"/>
<Setter Property="Background" Value="{DynamicResource TitleBarButtonHoverColor}"/>
<Setter Property="Foreground" Value="{DynamicResource AppFgColor}"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Background" Value="{DynamicResource TitlebarButtonPressed}"/>
</Trigger>
</Style.Triggers>
</Style>
<!-- SearchBox Border Style -->
<Style x:Key="SearchBoxBorderStyle" TargetType="Border">
<Setter Property="Background" Value="{DynamicResource ComboBgColor}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderColor}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="CornerRadius" Value="4"/>
<Setter Property="Width" Value="300"/>
<Setter Property="Padding" Value="8,6"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="{DynamicResource ComboHoverColor}"/>
<Setter Property="Background" Value="{DynamicResource TitleBarButtonPressedColor}"/>
<Setter Property="Foreground" Value="{DynamicResource AppFgColor}"/>
</Trigger>
</Style.Triggers>
</Style>
@@ -283,7 +250,7 @@
<!-- ContextMenu Style -->
<Style TargetType="ContextMenu">
<Setter Property="Background" Value="{DynamicResource CardBgColor}"/>
<Setter Property="BorderBrush" Value="{DynamicResource BorderColor}"/>
<Setter Property="BorderBrush" Value="{DynamicResource AppBorderColor}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="4"/>
<Setter Property="HasDropShadow" Value="True"/>
@@ -311,7 +278,7 @@
<!-- MenuItem Style -->
<Style TargetType="MenuItem">
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
<Setter Property="Foreground" Value="{DynamicResource AppFgColor}"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Padding" Value="8,6"/>
<Setter Property="Height" Value="32"/>
@@ -337,10 +304,10 @@
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsHighlighted" Value="True">
<Setter Property="Background" Value="{DynamicResource ComboItemHoverColor}"/>
<Setter Property="Background" Value="{DynamicResource ComboBoxItemHoverColor}"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Background" Value="{DynamicResource ComboItemSelectedColor}"/>
<Setter Property="Background" Value="{DynamicResource ComboBoxItemSelectedColor}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
@@ -349,10 +316,10 @@
</Style>
</Window.Resources>
<Border x:Name="MainBorder" BorderBrush="{DynamicResource BorderColor}"
<Border x:Name="MainBorder" BorderBrush="{DynamicResource AppBorderColor}"
BorderThickness="1"
CornerRadius="8"
Background="{DynamicResource BgColor}"
Background="{DynamicResource AppBgColor}"
Margin="0">
<Grid>
<Grid.RowDefinitions>
@@ -372,7 +339,7 @@
<!-- Custom Title Bar -->
<Grid Grid.Row="0" x:Name="TitleBar">
<Border x:Name="TitleBarBackground" Background="{DynamicResource BgColor}" CornerRadius="8,8,0,0">
<Border x:Name="TitleBarBackground" Background="{DynamicResource AppBgColor}" CornerRadius="8,8,0,0">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
@@ -380,13 +347,23 @@
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0"
Text="Win11Debloat"
Foreground="{DynamicResource FgColor}"
VerticalAlignment="Center"
Margin="12,0,0,0"
FontSize="12"/>
FontSize="12">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Foreground" Value="{DynamicResource AppFgColor}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsActive, RelativeSource={RelativeSource AncestorType=Window}}" Value="False">
<Setter Property="Foreground" Value="{DynamicResource TitleBarUnfocusedFgColor}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
<StackPanel Grid.Column="1" Orientation="Horizontal">
<Button x:Name="KofiBtn" shell:WindowChrome.IsHitTestVisibleInChrome="True" Content="&#xEB52;" 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="&#xE700;" FontFamily="Segoe Fluent Icons" FontSize="15" Style="{StaticResource TitlebarButton}" ToolTip="Options" AutomationProperties.Name="Options">
<Button x:Name="KofiBtn" shell:WindowChrome.IsHitTestVisibleInChrome="True" Content="&#xEB52;" FontSize="15" Style="{StaticResource TitlebarButton}" ToolTip="Support the creator" AutomationProperties.Name="Support the creator"/>
<Button x:Name="MenuBtn" shell:WindowChrome.IsHitTestVisibleInChrome="True" Content="&#xE700;" FontSize="15" Style="{StaticResource TitlebarButton}" ToolTip="Options" AutomationProperties.Name="Options">
<Button.ContextMenu>
<ContextMenu x:Name="MainMenu">
<ContextMenu.Resources>
@@ -395,7 +372,7 @@
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Separator">
<Border Height="1" Background="{DynamicResource BorderColor}" SnapsToDevicePixels="True" HorizontalAlignment="Stretch"/>
<Border Height="1" Background="{DynamicResource AppBorderColor}" SnapsToDevicePixels="True" HorizontalAlignment="Stretch"/>
</ControlTemplate>
</Setter.Value>
</Setter>
@@ -403,38 +380,38 @@
</ContextMenu.Resources>
<MenuItem x:Name="ImportConfigBtn" Header="Import config" AutomationProperties.Name="Import configuration">
<MenuItem.Icon>
<TextBlock Text="&#xe838;" FontFamily="Segoe Fluent Icons" FontSize="16" Foreground="{DynamicResource FgColor}"/>
<TextBlock Text="&#xe838;" 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="&#xe74e;" FontFamily="Segoe Fluent Icons" FontSize="16" Foreground="{DynamicResource FgColor}"/>
<TextBlock Text="&#xe74e;" 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="&#xe81c;" FontFamily="Segoe Fluent Icons" FontSize="16" Foreground="{DynamicResource FgColor}"/>
<TextBlock Text="&#xe81c;" 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="&#xe736;" FontFamily="Segoe MDL2 Assets" FontSize="16" Foreground="{DynamicResource FgColor}"/>
<TextBlock Text="&#xe736;" 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="&#xEBE8;" FontFamily="Segoe Fluent Icons" FontSize="16" Foreground="{DynamicResource FgColor}"/>
<TextBlock Text="&#xEBE8;" FontFamily="{DynamicResource AppIconFontFamily}" FontSize="16" Foreground="{DynamicResource AppFgColor}"/>
</MenuItem.Icon>
</MenuItem>
<MenuItem x:Name="MenuLogs" Header="Logs" AutomationProperties.Name="Logs">
<MenuItem.Icon>
<TextBlock Text="&#xf000;" FontFamily="Segoe Fluent Icons" FontSize="16" Foreground="{DynamicResource FgColor}"/>
<TextBlock Text="&#xf000;" FontFamily="{DynamicResource AppIconFontFamily}" FontSize="16" Foreground="{DynamicResource AppFgColor}"/>
</MenuItem.Icon>
</MenuItem>
<MenuItem x:Name="MenuAbout" Header="About" AutomationProperties.Name="About">
<MenuItem.Icon>
<TextBlock Text="&#xE946;" FontFamily="Segoe Fluent Icons" FontSize="16" Foreground="{DynamicResource FgColor}"/>
<TextBlock Text="&#xE946;" FontFamily="{DynamicResource AppIconFontFamily}" FontSize="16" Foreground="{DynamicResource AppFgColor}"/>
</MenuItem.Icon>
</MenuItem>
</ContextMenu>
@@ -459,16 +436,16 @@
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="TabItem">
<Border Name="Border" Background="{DynamicResource SecondaryButtonBg}" BorderThickness="0" CornerRadius="0" Padding="10,10" Margin="0">
<Border Name="Border" Background="{DynamicResource SecondaryButtonBgColor}" BorderThickness="0" CornerRadius="0" Padding="10,10" Margin="0">
<ContentPresenter ContentSource="Header" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="Border" Property="Background" Value="{DynamicResource ButtonBg}"/>
<Setter TargetName="Border" Property="Background" Value="{DynamicResource ButtonBgColor}"/>
<Setter Property="Foreground" Value="White"/>
</Trigger>
<Trigger Property="IsSelected" Value="False">
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
<Setter Property="Foreground" Value="{DynamicResource AppFgColor}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
@@ -487,73 +464,66 @@
<Grid>
<StackPanel x:Name="HomeContentPanel" HorizontalAlignment="Center" VerticalAlignment="Top">
<!-- Logo -->
<Viewbox Width="250" Height="250" Margin="0,0,0,10" HorizontalAlignment="Center">
<Viewbox Width="250" Height="250" Margin="0,0,0,16" HorizontalAlignment="Center">
<Grid Width="250" Height="250">
<!-- Windows logo style icon -->
<Path x:Name="LogoFallback" Data="M0,0 L80,0 L80,80 L0,80 Z M90,0 L170,0 L170,80 L90,80 Z M0,90 L80,90 L80,170 L0,170 Z M90,90 L170,90 L170,170 L90,170 Z"
Fill="{DynamicResource ButtonBg}" Stretch="Uniform" Margin="10"/>
Fill="{DynamicResource ButtonBgColor}" Stretch="Uniform" Margin="10"/>
<!-- Sparkle effects -->
<Canvas HorizontalAlignment="Right" VerticalAlignment="Bottom" Width="50" Height="50" Margin="0,0,2,2">
<Path Canvas.Left="10" Canvas.Top="16" Data="M12,0 L14,10 L24,12 L14,14 L12,24 L10,14 L0,12 L10,10 Z"
Fill="{DynamicResource AccentColor}" Width="40" Height="40" Stretch="Uniform"/>
<Canvas HorizontalAlignment="Right" VerticalAlignment="Bottom" Width="80" Height="80" Margin="0,0,2,2">
<Path Canvas.Left="12" Canvas.Top="32" Data="M12,0 L14,10 L24,12 L14,14 L12,24 L10,14 L0,12 L10,10 Z"
Fill="{DynamicResource AppAccentColor}" Width="60" Height="60" Stretch="Uniform"/>
<Path Canvas.Left="0" Canvas.Top="0" Data="M6,0 L7,5 L12,6 L7,7 L6,12 L5,7 L0,6 L5,5 Z"
Fill="{DynamicResource AccentColor}" Width="22" Height="22" Stretch="Uniform"/>
<Path Canvas.Left="35" Canvas.Top="8" Data="M4,0 L5,3 L8,4 L5,5 L4,8 L3,5 L0,4 L3,3 Z"
Fill="{DynamicResource AccentColor}" Width="17" Height="17" Stretch="Uniform"/>
Fill="{DynamicResource AppAccentColor}" Width="40" Height="40" Stretch="Uniform"/>
<Path Canvas.Left="55" Canvas.Top="16" Data="M4,0 L5,3 L8,4 L5,5 L4,8 L3,5 L0,4 L3,3 Z"
Fill="{DynamicResource AppAccentColor}" Width="25" Height="25" Stretch="Uniform"/>
</Canvas>
</Grid>
</Viewbox>
<!-- Title -->
<TextBlock Text="Welcome to Win11Debloat" FontSize="30" FontWeight="Bold" Foreground="{DynamicResource FgColor}" HorizontalAlignment="Center" Margin="0,0,0,10"/>
<TextBlock TextWrapping="Wrap" Foreground="{DynamicResource FgColor}" FontSize="18" LineHeight="22" HorizontalAlignment="Center" Margin="0,0,0,60">
<TextBlock Text="Welcome to Win11Debloat" FontSize="40" FontWeight="SemiBold" Foreground="{DynamicResource AppFgColor}" HorizontalAlignment="Center"/>
<TextBlock TextWrapping="Wrap" Foreground="{DynamicResource AppFgColor}" FontSize="20" HorizontalAlignment="Center" Margin="0,4,0,48">
<Run Text="Your clean Windows experience is just a few clicks away!"/>
</TextBlock>
<Border HorizontalAlignment="Center" BorderBrush="{DynamicResource BorderColor}" BorderThickness="1" CornerRadius="4" Background="{DynamicResource CardBgColor}" Padding="16,12" Width="500">
<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)"/>
</ComboBox>
<StackPanel x:Name="OtherUserPanel" Visibility="Collapsed" Margin="0,0,0,6">
<TextBlock x:Name="UsernameValidationMessage" Text="" FontStyle="Italic" Foreground="{DynamicResource CloseHover}" FontSize="11" Margin="3,0,0,4" TextWrapping="Wrap"/>
<Border IsEnabled="{Binding ElementName=OtherUsernameTextBox, Path=IsEnabled}">
<Border.Style>
<Style TargetType="Border" BasedOn="{StaticResource UserTextBoxBorderStyle}">
<Style.Triggers>
<DataTrigger Binding="{Binding ElementName=OtherUsernameTextBox, Path=IsFocused}" Value="True">
<Setter Property="Background" Value="{DynamicResource InputFocusColor}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="&#xE77B;" FontFamily="Segoe Fluent Icons" FontSize="14" VerticalAlignment="Center" Margin="4,0,8,0" Foreground="{DynamicResource FgColor}"/>
<TextBlock x:Name="UsernameTextBoxPlaceholder" Grid.Column="1" Text="Enter username" Foreground="{DynamicResource FgColor}" Opacity="0.5" FontSize="13" Margin="3,0,0,1" VerticalAlignment="Center" IsHitTestVisible="False"/>
<TextBox x:Name="OtherUsernameTextBox" Grid.Column="1" Style="{StaticResource UserTextBoxStyle}" Text="" AutomationProperties.Name="Enter username"/>
</Grid>
<TextBlock x:Name="UsernameValidationMessage" Text="" FontStyle="Italic" Foreground="{DynamicResource TitleBarCloseHoverColor}" FontSize="11" Margin="3,0,0,4" TextWrapping="Wrap"/>
<Border Style="{DynamicResource TextBoxBorderStyle}">
<Border Style="{DynamicResource TextBoxBottomBorderStyle}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</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="&#xE77B;" FontFamily="{DynamicResource AppIconFontFamily}" FontSize="14" VerticalAlignment="Center" Margin="8,0,4,0" Foreground="{DynamicResource AppFgColor}" Opacity="0.7"/>
</Grid>
</Border>
</Border>
</StackPanel>
<Separator Margin="0,10,0,8" Background="{DynamicResource BorderColor}"/>
<Separator Margin="0,10,0,8" Background="{DynamicResource AppBorderColor}"/>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,8,0,0">
<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="&#xe735;" FontFamily="Segoe Fluent Icons" FontSize="16" VerticalAlignment="Center" Margin="0,0,8,-1"/>
<TextBlock Text="&#xe735;" 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="&#xe90f;" FontFamily="Segoe Fluent Icons" FontSize="14" VerticalAlignment="Center" Margin="0,0,8,-1"/>
<TextBlock Text="&#xe90f;" 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>
@@ -567,10 +537,10 @@
<!-- App Removal Tab -->
<TabItem Header="App Removal">
<DockPanel>
<Border DockPanel.Dock="Top" Padding="20,10,20,0">
<Border DockPanel.Dock="Top" Padding="20,4,20,0">
<StackPanel>
<TextBlock Text="App Removal" FontWeight="Bold" FontSize="20" Margin="0,0,0,5" Foreground="{DynamicResource FgColor}"/>
<TextBlock Text="Select which apps you want to remove from your system" FontSize="13" Margin="0,0,0,20" Foreground="{DynamicResource FgColor}" TextWrapping="Wrap"/>
<TextBlock Text="App Removal" FontWeight="SemiBold" FontSize="24" Foreground="{DynamicResource AppFgColor}"/>
<TextBlock Text="Select which apps you want to remove" FontSize="14" Margin="0,5,0,20" Foreground="{DynamicResource AppFgColor}" TextWrapping="Wrap"/>
<!-- Filter Options -->
<Grid Margin="0,0,0,12">
@@ -583,11 +553,11 @@
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal" Grid.Column="0">
<ToggleButton x:Name="PresetsBtn" ToolTip="Select or clear app presets" Height="32" Padding="10,0" Margin="0,0,10,0" AutomationProperties.Name="App Presets">
<ToggleButton x:Name="PresetsBtn" Height="32" Padding="10,0" Margin="0,0,10,0" AutomationProperties.Name="App Presets">
<ToggleButton.Style>
<Style TargetType="ToggleButton">
<Setter Property="Background" Value="{DynamicResource SecondaryButtonBg}"/>
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
<Setter Property="Background" Value="{DynamicResource SecondaryButtonBgColor}"/>
<Setter Property="Foreground" Value="{DynamicResource AppFgColor}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderColor}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="FontSize" Value="14"/>
@@ -603,21 +573,21 @@
</Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="{DynamicResource SecondaryButtonHover}"/>
<Setter Property="Background" Value="{DynamicResource SecondaryButtonHoverColor}"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Background" Value="{DynamicResource SecondaryButtonPressed}"/>
<Setter Property="Background" Value="{DynamicResource SecondaryButtonPressedColor}"/>
</Trigger>
<Trigger Property="IsChecked" Value="True">
<Setter Property="Background" Value="{DynamicResource SecondaryButtonHover}"/>
<Setter Property="Background" Value="{DynamicResource SecondaryButtonHoverColor}"/>
</Trigger>
</Style.Triggers>
</Style>
</ToggleButton.Style>
<StackPanel Orientation="Horizontal">
<TextBlock Text="&#xE735;" FontFamily="Segoe Fluent Icons" FontSize="14" VerticalAlignment="Center" Margin="0,0,6,0"/>
<TextBlock Text="Quick Select" FontSize="13" VerticalAlignment="Center" Margin="0,0,6,0"/>
<TextBlock x:Name="PresetsArrow" Text="&#xE70D;" FontFamily="Segoe Fluent Icons" FontSize="10" VerticalAlignment="Center" RenderTransformOrigin="0.5,0.5">
<TextBlock Text="&#xE735;" 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="&#xE70D;" FontFamily="{DynamicResource AppIconFontFamily}" FontSize="10" VerticalAlignment="Center" RenderTransformOrigin="0.5,0.5">
<TextBlock.RenderTransform>
<RotateTransform x:Name="PresetsArrowRotation" Angle="0"/>
</TextBlock.RenderTransform>
@@ -626,46 +596,41 @@
</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="&#xE711;" FontFamily="Segoe Fluent Icons" FontSize="14" VerticalAlignment="Center" Margin="0,0,6,0"/>
<TextBlock Text="&#xE711;" 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>
<Popup x:Name="PresetsPopup" PlacementTarget="{Binding ElementName=PresetsBtn}" Placement="Bottom" StaysOpen="True" AllowsTransparency="True" VerticalOffset="2">
<Border Background="{DynamicResource CardBgColor}" BorderBrush="{DynamicResource BorderColor}" BorderThickness="1" CornerRadius="6" Padding="4,6" Margin="12">
<Border Background="{DynamicResource CardBgColor}" BorderBrush="{DynamicResource AppBorderColor}" BorderThickness="1" CornerRadius="6" Padding="4,6" Margin="12">
<Border.Effect>
<DropShadowEffect BlurRadius="12" Opacity="0.25" ShadowDepth="4"/>
</Border.Effect>
<StackPanel x:Name="PresetsPanel" MinWidth="220">
<CheckBox x:Name="PresetDefaultApps" Content="Default apps" IsThreeState="True" ToolTip="Select the apps that are safe to remove for most users" Foreground="{DynamicResource FgColor}" Margin="8,4" AutomationProperties.Name="Default selection"/>
<CheckBox x:Name="PresetLastUsed" Content="Last used apps" IsThreeState="True" ToolTip="Select the apps that were removed the last time Win11Debloat was run" Foreground="{DynamicResource FgColor}" Margin="8,4" AutomationProperties.Name="Last used selection"/>
<Separator Margin="4,6" Background="{DynamicResource BorderColor}"/>
<CheckBox x:Name="PresetDefaultApps" Content="Default apps" IsThreeState="True" ToolTip="Select the apps that are safe to remove for most users" Foreground="{DynamicResource AppFgColor}" Margin="8,4" AutomationProperties.Name="Default selection"/>
<CheckBox x:Name="PresetLastUsed" Content="Previously selected apps" IsThreeState="True" ToolTip="Select the apps that were removed the last time Win11Debloat was run" Foreground="{DynamicResource AppFgColor}" Margin="8,4" AutomationProperties.Name="Previously selected apps"/>
<Separator Margin="4,6" Background="{DynamicResource AppBorderColor}"/>
<StackPanel x:Name="JsonPresetsPanel"/>
</StackPanel>
</Border>
</Popup>
</StackPanel>
<CheckBox x:Name="OnlyInstalledAppsBox" Grid.Column="2" Content="Only show installed apps" IsChecked="False" Foreground="{DynamicResource FgColor}" VerticalAlignment="Center" AutomationProperties.Name="Only show installed apps"/>
<CheckBox x:Name="OnlyInstalledAppsBox" Grid.Column="2" Content="Only show installed apps" ToolTip="Only show applications that are currently installed on this system" IsChecked="False" Foreground="{DynamicResource AppFgColor}" VerticalAlignment="Center" AutomationProperties.Name="Only show installed apps"/>
<Border x:Name="AppSearchBorder" Grid.Column="4">
<Border.Style>
<Style TargetType="Border" BasedOn="{StaticResource SearchBoxBorderStyle}">
<Style.Triggers>
<DataTrigger Binding="{Binding ElementName=AppSearchBox, Path=IsFocused}" Value="True">
<Setter Property="Background" Value="{DynamicResource InputFocusColor}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="&#xE721;" FontFamily="Segoe Fluent Icons" FontSize="14" VerticalAlignment="Center" Margin="4,0,8,0" Foreground="{DynamicResource FgColor}"/>
<TextBlock x:Name="AppSearchPlaceholder" Grid.Column="1" Text="Search app" Foreground="{DynamicResource FgColor}" Opacity="0.5" FontSize="13" Margin="3,0,0,1" VerticalAlignment="Center" IsHitTestVisible="False"/>
<TextBox x:Name="AppSearchBox" Grid.Column="1" Background="Transparent" Foreground="{DynamicResource FgColor}" BorderThickness="0" FontSize="13" Margin="1,0,0,1" VerticalAlignment="Center" Text="" AutomationProperties.Name="Search app"/>
</Grid>
<Border x:Name="AppSearchBorder" Grid.Column="4" Width="300">
<Border Style="{DynamicResource TextBoxBorderStyle}">
<Border Style="{DynamicResource TextBoxBottomBorderStyle}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</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="&#xE721;" FontFamily="{DynamicResource AppIconFontFamily}" FontSize="14" VerticalAlignment="Center" Margin="8,0,4,0" Foreground="{DynamicResource AppFgColor}" Opacity="0.7"/>
</Grid>
</Border>
</Border>
</Border>
</Grid>
</StackPanel>
@@ -683,7 +648,7 @@
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- Column Headers -->
<Border Grid.Row="0" Background="{DynamicResource TableHeaderColor}" BorderBrush="{DynamicResource BorderColor}" BorderThickness="1,1,1,0" CornerRadius="4,4,0,0">
<Border Grid.Row="0" Background="{DynamicResource TableHeaderColor}" BorderBrush="{DynamicResource AppBorderColor}" BorderThickness="1,1,1,0" CornerRadius="4,4,0,0">
<Grid Margin="42,6,23,8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="{StaticResource AppTableDotColWidth}"/>
@@ -692,34 +657,34 @@
<ColumnDefinition Width="{StaticResource AppTableIdColWidth}"/>
</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 FgColor}"/>
<TextBlock x:Name="SortArrowName" Text="&#xE70E;" FontFamily="Segoe Fluent Icons" FontSize="11" Foreground="{DynamicResource FgColor}" VerticalAlignment="Center" Margin="5,1,0,0" Opacity="0.3" RenderTransformOrigin="0.5,0.5">
<TextBlock Text="Name" FontWeight="SemiBold" FontSize="16" Foreground="{DynamicResource AppFgColor}"/>
<TextBlock x:Name="SortArrowName" Text="&#xE70E;" 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 FgColor}"/>
<TextBlock x:Name="SortArrowDescription" Text="&#xE70E;" FontFamily="Segoe Fluent Icons" FontSize="11" Foreground="{DynamicResource FgColor}" VerticalAlignment="Center" Margin="5,1,0,0" Opacity="0.3" RenderTransformOrigin="0.5,0.5">
<TextBlock Text="Description" FontWeight="SemiBold" FontSize="16" Foreground="{DynamicResource AppFgColor}"/>
<TextBlock x:Name="SortArrowDescription" Text="&#xE70E;" 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 FgColor}"/>
<TextBlock x:Name="SortArrowAppId" Text="&#xE70E;" FontFamily="Segoe Fluent Icons" FontSize="11" Foreground="{DynamicResource FgColor}" VerticalAlignment="Center" Margin="5,1,0,0" Opacity="0.3" RenderTransformOrigin="0.5,0.5">
<TextBlock Text="App ID" FontWeight="SemiBold" FontSize="16" Foreground="{DynamicResource AppFgColor}"/>
<TextBlock x:Name="SortArrowAppId" Text="&#xE70E;" 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>
</Grid>
</Border>
<!-- Apps content -->
<Border Grid.Row="1" BorderBrush="{DynamicResource BorderColor}" CornerRadius="0,0,4,4" BorderThickness="1" Background="{DynamicResource CardBgColor}" Padding="0,1,1,1">
<Border Grid.Row="1" BorderBrush="{DynamicResource AppBorderColor}" CornerRadius="0,0,4,4" BorderThickness="1" Background="{DynamicResource CardBgColor}" Padding="0,1,1,1">
<Grid>
<ScrollViewer VerticalScrollBarVisibility="Visible">
<StackPanel x:Name="AppSelectionPanel" Margin="10,4,0,4"/>
</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="&#xF16A;" FontFamily="Segoe Fluent Icons" FontSize="28" Foreground="{DynamicResource FgColor}" HorizontalAlignment="Center" Margin="0,0,0,8" RenderTransformOrigin="0.5,0.5">
<TextBlock Text="&#xF16A;" 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>
@@ -736,17 +701,39 @@
</EventTrigger>
</TextBlock.Triggers>
</TextBlock>
<TextBlock Text="Loading apps..." FontSize="16" FontWeight="SemiBold" Foreground="{DynamicResource FgColor}" HorizontalAlignment="Center"/>
<TextBlock Text="Loading apps..." FontSize="16" FontWeight="SemiBold" Foreground="{DynamicResource AppFgColor}" HorizontalAlignment="Center"/>
</StackPanel>
</Border>
</Grid>
</Border>
</Grid>
<!-- Status Info -->
<StackPanel Grid.Row="1" Orientation="Horizontal" Margin="20,0,20,0">
<TextBlock x:Name="AppSelectionStatus" Text="" Foreground="{DynamicResource FgColor}" Margin="10,0,0,5" HorizontalAlignment="Left"/>
</StackPanel>
<!-- Status Info & Color Legend -->
<Grid Grid.Row="1" Margin="20,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock x:Name="AppSelectionStatus" Grid.Column="0" Text="" Foreground="{DynamicResource AppFgColor}" Margin="10,0,0,5" HorizontalAlignment="Left" VerticalAlignment="Bottom"/>
<StackPanel x:Name="AppColorLegend" Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Bottom" Margin="0,0,10,5">
<TextBlock Text="Legend:" Foreground="{DynamicResource AppFgColor}" FontSize="12" FontWeight="SemiBold" VerticalAlignment="Center" Margin="0,0,10,0" Opacity="0.8"/>
<!-- Recommended (safe) -->
<StackPanel Orientation="Horizontal" Margin="0,0,16,0" ToolTip="[Recommended] Safe to remove for most users">
<Ellipse Width="9" Height="9" Fill="{DynamicResource AppRecommendationSafeColor}" VerticalAlignment="Center" Margin="0,0,6,0"/>
<TextBlock Text="Recommended" Foreground="{DynamicResource AppFgColor}" FontSize="12" VerticalAlignment="Center" Opacity="0.8"/>
</StackPanel>
<!-- Optional (default) -->
<StackPanel Orientation="Horizontal" Margin="0,0,16,0" ToolTip="[Optional] Can be safely removed if you don't need this app">
<Ellipse Width="9" Height="9" Fill="{DynamicResource AppRecommendationOptionalColor}" VerticalAlignment="Center" Margin="0,0,6,0"/>
<TextBlock Text="Optional" Foreground="{DynamicResource AppFgColor}" FontSize="12" VerticalAlignment="Center" Opacity="0.8"/>
</StackPanel>
<!-- Not Recommended (unsafe) -->
<StackPanel Orientation="Horizontal" ToolTip="[Not Recommended] Only remove if you know what you are doing">
<Ellipse Width="9" Height="9" Fill="{DynamicResource AppRecommendationUnsafeColor}" VerticalAlignment="Center" Margin="0,0,6,0"/>
<TextBlock Text="Not Recommended" Foreground="{DynamicResource AppFgColor}" FontSize="12" VerticalAlignment="Center" Opacity="0.8"/>
</StackPanel>
</StackPanel>
</Grid>
</Grid>
</DockPanel>
</TabItem>
@@ -754,10 +741,10 @@
<!-- Tweaks Tab -->
<TabItem Header="Tweaks">
<DockPanel>
<Border DockPanel.Dock="Top" Padding="20,10,20,0" Background="{DynamicResource BgColor}">
<Border DockPanel.Dock="Top" Padding="20,4,20,0" Background="{DynamicResource AppBgColor}">
<StackPanel>
<TextBlock Text="System Tweaks" FontWeight="Bold" FontSize="20" Margin="0,0,0,5" Foreground="{DynamicResource FgColor}"/>
<TextBlock Text="Select which tweaks you want to apply to your system, hover over settings for more information" FontSize="13" Margin="0,0,0,20" Foreground="{DynamicResource FgColor}" TextWrapping="Wrap"/>
<TextBlock Text="System Tweaks" FontWeight="SemiBold" FontSize="24" Foreground="{DynamicResource AppFgColor}"/>
<TextBlock Text="Select what changes you want to make, you can hover over settings for more information" FontSize="14" Margin="0,5,0,20" Foreground="{DynamicResource AppFgColor}" TextWrapping="Wrap"/>
<!-- Filter Options -->
<Grid Margin="0,0,0,12">
@@ -770,11 +757,11 @@
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" Orientation="Horizontal">
<ToggleButton x:Name="TweaksPresetsBtn" ToolTip="Select tweak presets" Height="32" Padding="10,0" Margin="0,0,10,0" AutomationProperties.Name="Tweak Presets">
<ToggleButton x:Name="TweaksPresetsBtn" Height="32" Padding="10,0" Margin="0,0,10,0" AutomationProperties.Name="Tweak Presets">
<ToggleButton.Style>
<Style TargetType="ToggleButton">
<Setter Property="Background" Value="{DynamicResource SecondaryButtonBg}"/>
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
<Setter Property="Background" Value="{DynamicResource SecondaryButtonBgColor}"/>
<Setter Property="Foreground" Value="{DynamicResource AppFgColor}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderColor}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="FontSize" Value="14"/>
@@ -790,21 +777,21 @@
</Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="{DynamicResource SecondaryButtonHover}"/>
<Setter Property="Background" Value="{DynamicResource SecondaryButtonHoverColor}"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Background" Value="{DynamicResource SecondaryButtonPressed}"/>
<Setter Property="Background" Value="{DynamicResource SecondaryButtonPressedColor}"/>
</Trigger>
<Trigger Property="IsChecked" Value="True">
<Setter Property="Background" Value="{DynamicResource SecondaryButtonHover}"/>
<Setter Property="Background" Value="{DynamicResource SecondaryButtonHoverColor}"/>
</Trigger>
</Style.Triggers>
</Style>
</ToggleButton.Style>
<StackPanel Orientation="Horizontal">
<TextBlock Text="&#xE735;" FontFamily="Segoe Fluent Icons" FontSize="14" VerticalAlignment="Center" Margin="0,1,6,0"/>
<TextBlock Text="Quick Select" FontSize="13" VerticalAlignment="Center" Margin="0,0,6,0"/>
<TextBlock x:Name="TweaksPresetsArrow" Text="&#xE70D;" FontFamily="Segoe Fluent Icons" FontSize="10" VerticalAlignment="Center" RenderTransformOrigin="0.5,0.5">
<TextBlock Text="&#xE735;" 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="&#xE70D;" FontFamily="{DynamicResource AppIconFontFamily}" FontSize="10" VerticalAlignment="Center" RenderTransformOrigin="0.5,0.5">
<TextBlock.RenderTransform>
<RotateTransform Angle="0"/>
</TextBlock.RenderTransform>
@@ -813,47 +800,42 @@
</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="&#xE711;" FontFamily="Segoe Fluent Icons" FontSize="14" VerticalAlignment="Center" Margin="0,0,6,0"/>
<TextBlock Text="&#xE711;" 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>
<Popup x:Name="TweaksPresetsPopup" PlacementTarget="{Binding ElementName=TweaksPresetsBtn}" Placement="Bottom" StaysOpen="True" AllowsTransparency="True" VerticalOffset="2">
<Border Background="{DynamicResource CardBgColor}" BorderBrush="{DynamicResource BorderColor}" BorderThickness="1" CornerRadius="6" Padding="4,6" Margin="12">
<Border Background="{DynamicResource CardBgColor}" BorderBrush="{DynamicResource AppBorderColor}" BorderThickness="1" CornerRadius="6" Padding="4,6" Margin="12">
<Border.Effect>
<DropShadowEffect BlurRadius="12" Opacity="0.25" ShadowDepth="4"/>
</Border.Effect>
<StackPanel MinWidth="220">
<CheckBox x:Name="PresetDefaultTweaksBtn" Content="Default settings" IsThreeState="True" ToolTip="Select the settings that are recommended for most people" Foreground="{DynamicResource FgColor}" Style="{DynamicResource PresetCheckBoxStyle}" AutomationProperties.Name="Default settings"/>
<CheckBox x:Name="PresetLastUsedTweaksBtn" Content="Last used settings" IsThreeState="True" ToolTip="Select the settings that were used the last time Win11Debloat was run" Foreground="{DynamicResource FgColor}" Style="{DynamicResource PresetCheckBoxStyle}" AutomationProperties.Name="Last used settings"/>
<Separator Margin="4,6" Background="{DynamicResource BorderColor}"/>
<CheckBox x:Name="PresetPrivacyTweaksBtn" Content="Privacy &amp; Suggested Content" IsThreeState="True" ToolTip="Select all Privacy &amp; Suggested Content tweaks" Foreground="{DynamicResource FgColor}" Style="{DynamicResource PresetCheckBoxStyle}" AutomationProperties.Name="All Privacy and Suggested Content"/>
<CheckBox x:Name="PresetAITweaksBtn" Content="AI features" IsThreeState="True" ToolTip="Select all AI feature tweaks" Foreground="{DynamicResource FgColor}" Style="{DynamicResource PresetCheckBoxStyle}" AutomationProperties.Name="All AI features"/>
<CheckBox x:Name="PresetDefaultTweaksBtn" Content="Default settings" IsThreeState="True" ToolTip="Select the settings that are recommended for most people" Foreground="{DynamicResource AppFgColor}" Style="{DynamicResource PresetCheckBoxStyle}" AutomationProperties.Name="Default settings"/>
<CheckBox x:Name="PresetLastUsedTweaksBtn" Content="Previously selected settings" IsThreeState="True" ToolTip="Select the settings that were applied the last time Win11Debloat was run" Foreground="{DynamicResource AppFgColor}" Style="{DynamicResource PresetCheckBoxStyle}" AutomationProperties.Name="Previously selected settings"/>
<Separator Margin="4,6" Background="{DynamicResource AppBorderColor}"/>
<CheckBox x:Name="PresetPrivacyTweaksBtn" Content="Privacy &amp; suggested content" IsThreeState="True" ToolTip="Select all tweaks related to privacy &amp; suggested content" Foreground="{DynamicResource AppFgColor}" Style="{DynamicResource PresetCheckBoxStyle}" AutomationProperties.Name="All Privacy and Suggested Content"/>
<CheckBox x:Name="PresetAITweaksBtn" Content="AI features" IsThreeState="True" ToolTip="Select all AI related tweaks" Foreground="{DynamicResource AppFgColor}" Style="{DynamicResource PresetCheckBoxStyle}" AutomationProperties.Name="All AI features"/>
</StackPanel>
</Border>
</Popup>
</StackPanel>
<CheckBox x:Name="ShowCurrentlyAppliedTweaksCheckBox" Grid.Column="2" Content="Detect applied tweaks" IsChecked="True" Foreground="{DynamicResource FgColor}" VerticalAlignment="Center" AutomationProperties.Name="Detect applied tweaks" ToolTip="Detect all tweaks currently applied for the current user."/>
<CheckBox x:Name="ShowCurrentlyAppliedTweaksCheckBox" Grid.Column="2" Content="Detect applied tweaks" IsChecked="True" Foreground="{DynamicResource AppFgColor}" VerticalAlignment="Center" AutomationProperties.Name="Detect applied tweaks" ToolTip="Detect all tweaks currently applied for the current user"/>
<Border x:Name="TweakSearchBorder" Grid.Column="4">
<Border.Style>
<Style TargetType="Border" BasedOn="{StaticResource SearchBoxBorderStyle}">
<Style.Triggers>
<DataTrigger Binding="{Binding ElementName=TweakSearchBox, Path=IsFocused}" Value="True">
<Setter Property="Background" Value="{DynamicResource InputFocusColor}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="&#xE721;" FontFamily="Segoe Fluent Icons" FontSize="14" VerticalAlignment="Center" Margin="4,0,8,0" Foreground="{DynamicResource FgColor}"/>
<TextBlock x:Name="TweakSearchPlaceholder" Grid.Column="1" Text="Search setting" Foreground="{DynamicResource FgColor}" Opacity="0.5" FontSize="13" Margin="3,0,0,1" VerticalAlignment="Center" IsHitTestVisible="False"/>
<TextBox x:Name="TweakSearchBox" Grid.Column="1" Background="Transparent" Foreground="{DynamicResource FgColor}" BorderThickness="0" FontSize="13" Margin="1,0,0,1" VerticalAlignment="Center" Text=""/>
</Grid>
<Border x:Name="TweakSearchBorder" Grid.Column="4" Width="300">
<Border Style="{DynamicResource TextBoxBorderStyle}">
<Border Style="{DynamicResource TextBoxBottomBorderStyle}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</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="&#xE721;" FontFamily="{DynamicResource AppIconFontFamily}" FontSize="14" VerticalAlignment="Center" Margin="8,0,4,0" Foreground="{DynamicResource AppFgColor}" Opacity="0.7"/>
</Grid>
</Border>
</Border>
</Border>
</Grid>
</StackPanel>
@@ -878,7 +860,7 @@
</Grid>
</ScrollViewer>
<!-- Top fade gradient -->
<Border IsHitTestVisible="False" VerticalAlignment="Top" Height="10" Margin="0,-12,20,0" Background="{DynamicResource BgColor}">
<Border IsHitTestVisible="False" VerticalAlignment="Top" Height="10" Margin="0,-12,20,0" Background="{DynamicResource AppBgColor}">
<Border.OpacityMask>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<GradientStop Color="Black" Offset="0"/>
@@ -888,7 +870,7 @@
</Border.OpacityMask>
</Border>
<!-- Bottom fade gradient -->
<Border IsHitTestVisible="False" VerticalAlignment="Bottom" Height="20" Margin="0,0,20,0" Background="{DynamicResource BgColor}">
<Border IsHitTestVisible="False" VerticalAlignment="Bottom" Height="20" Margin="0,0,20,0" Background="{DynamicResource AppBgColor}">
<Border.OpacityMask>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<GradientStop Color="Transparent" Offset="0"/>
@@ -903,10 +885,10 @@
<!-- Deployment Settings Tab -->
<TabItem Header="Deployment Settings" x:Name="DeploymentSettingsTab">
<DockPanel>
<Border DockPanel.Dock="Top" Padding="20,10,20,0">
<Border DockPanel.Dock="Top" Padding="20,4,20,0">
<StackPanel>
<TextBlock Text="Deployment Settings" FontWeight="Bold" FontSize="20" Margin="0,0,0,5" Foreground="{DynamicResource FgColor}"/>
<TextBlock Text="Configure how your changes will be applied and more" FontSize="13" Margin="0,0,0,20" Foreground="{DynamicResource FgColor}" TextWrapping="Wrap"/>
<TextBlock Text="Deployment Settings" FontWeight="SemiBold" FontSize="24" Foreground="{DynamicResource AppFgColor}"/>
<TextBlock Text="Configure how the selected changes will be applied to your system" FontSize="14" Margin="0,5,0,20" Foreground="{DynamicResource AppFgColor}" TextWrapping="Wrap"/>
</StackPanel>
</Border>
@@ -926,15 +908,15 @@
</Grid.RowDefinitions>
<!-- Apply Changes To -->
<Border Grid.Row="0" BorderBrush="{DynamicResource BorderColor}" BorderThickness="1" CornerRadius="4" Background="{DynamicResource CardBgColor}" Padding="16,12" Margin="0,0,0,16">
<Border Grid.Row="0" BorderBrush="{DynamicResource AppBorderColor}" BorderThickness="1" CornerRadius="4" Background="{DynamicResource CardBgColor}" Padding="16,12" Margin="0,0,0,16">
<StackPanel>
<TextBlock Text="Changes will be applied to" Style="{StaticResource CategoryHeaderTextBlock}"/>
<TextBlock x:Name="UserSelectionDescription" Text="The currently logged-in user profile." Foreground="{DynamicResource FgColor}" FontSize="12" TextWrapping="Wrap" Margin="0,0,0,3"/>
<TextBlock x:Name="UserSelectionDescription" Text="The currently logged-in user profile." Foreground="{DynamicResource AppFgColor}" FontSize="13" TextWrapping="Wrap" Margin="0,-2,0,3"/>
</StackPanel>
</Border>
<!-- App Removal Scope -->
<Border Grid.Row="1" x:Name="AppRemovalScopeSection" BorderBrush="{DynamicResource BorderColor}" BorderThickness="1" CornerRadius="4" Background="{DynamicResource CardBgColor}" Padding="16,12" Margin="0,0,0,16">
<Border Grid.Row="1" x:Name="AppRemovalScopeSection" BorderBrush="{DynamicResource AppBorderColor}" BorderThickness="1" CornerRadius="4" Background="{DynamicResource CardBgColor}" Padding="16,12" Margin="0,0,0,16">
<StackPanel>
<TextBlock Text="Apps will be removed for" Style="{StaticResource CategoryHeaderTextBlock}"/>
<ComboBox x:Name="AppRemovalScopeCombo" Margin="0,0,0,6" AutomationProperties.Name="App Removal Scope">
@@ -942,21 +924,23 @@
<ComboBoxItem x:Name="AppRemovalScopeCurrentUser" Content="Current user only"/>
<ComboBoxItem x:Name="AppRemovalScopeTargetUser" Content="Target user only" Visibility="Collapsed"/>
</ComboBox>
<TextBlock x:Name="AppRemovalScopeDescription" Text="Apps will be removed for all users and from the Windows image to prevent reinstallation for new users." Foreground="{DynamicResource FgColor}" FontSize="12" TextWrapping="Wrap" Margin="0,6,0,3"/>
<TextBlock x:Name="AppRemovalScopeDescription" Text="Apps will be removed for all users and from the Windows image to prevent reinstallation for new users." Foreground="{DynamicResource AppFgColor}" FontSize="13" TextWrapping="Wrap" Margin="0,6,0,3"/>
</StackPanel>
</Border>
<!-- Options -->
<Border Grid.Row="2" BorderBrush="{DynamicResource BorderColor}" BorderThickness="1" CornerRadius="4" Background="{DynamicResource CardBgColor}" Padding="16,12,16,3" Margin="0,0,0,16">
<Border Grid.Row="2" BorderBrush="{DynamicResource AppBorderColor}" BorderThickness="1" CornerRadius="4" Background="{DynamicResource CardBgColor}" Padding="16,12,16,3" Margin="0,0,0,16">
<StackPanel>
<TextBlock Text="Options" Style="{StaticResource CategoryHeaderTextBlock}"/>
<!-- Restore Point Option -->
<StackPanel>
<CheckBox x:Name="RegistryBackupCheckBox" Style="{DynamicResource FeatureCheckboxStyle}" IsChecked="True" Content="Create a registry backup (Recommended)" AutomationProperties.Name="Create a registry backup (Recommended)"/>
</StackPanel>
<StackPanel>
<CheckBox x:Name="RestorePointCheckBox" Style="{DynamicResource FeatureCheckboxStyle}" IsChecked="True" Content="Create a system restore point (Recommended)" AutomationProperties.Name="Create a system restore point (Recommended)"/>
</StackPanel>
<!-- Restart Explorer Option -->
<StackPanel>
<CheckBox x:Name="RestartExplorerCheckBox" Style="{DynamicResource FeatureCheckboxStyle}" Content="Restart the Windows Explorer process to apply all changes immediately" AutomationProperties.Name="Restart the Windows Explorer process to apply all changes immediately"/>
</StackPanel>
@@ -965,7 +949,7 @@
</Grid>
</ScrollViewer>
<!-- Top fade gradient -->
<Border IsHitTestVisible="False" VerticalAlignment="Top" Height="10" Margin="0,-12,20,0" Background="{DynamicResource BgColor}">
<Border IsHitTestVisible="False" VerticalAlignment="Top" Height="10" Margin="0,-12,20,0" Background="{DynamicResource AppBgColor}">
<Border.OpacityMask>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<GradientStop Color="Black" Offset="0"/>
@@ -975,7 +959,7 @@
</Border.OpacityMask>
</Border>
<!-- Bottom fade gradient -->
<Border IsHitTestVisible="False" VerticalAlignment="Bottom" Height="20" Margin="0,0,20,0" Background="{DynamicResource BgColor}">
<Border IsHitTestVisible="False" VerticalAlignment="Bottom" Height="20" Margin="0,0,20,0" Background="{DynamicResource AppBgColor}">
<Border.OpacityMask>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<GradientStop Color="Transparent" Offset="0"/>
@@ -986,25 +970,25 @@
</Grid>
<!-- Review & Apply Section -->
<StackPanel Grid.Row="1" HorizontalAlignment="Stretch" Background="{DynamicResource BgColor}">
<StackPanel Grid.Row="1" HorizontalAlignment="Stretch" Background="{DynamicResource AppBgColor}">
<Button x:Name="ReviewChangesBtn" Background="Transparent" BorderThickness="0" Cursor="Hand" HorizontalAlignment="Center" Margin="0,4,0,10" AutomationProperties.Name="Review selected changes">
<Button.Template>
<ControlTemplate TargetType="Button">
<TextBlock x:Name="LinkText" Text="Review selected changes" FontSize="14" Foreground="{DynamicResource ButtonBg}" FontWeight="SemiBold" HorizontalAlignment="Center"/>
<TextBlock x:Name="LinkText" Text="Review selected changes" FontSize="16" Foreground="{DynamicResource ButtonBgColor}" FontWeight="SemiBold" HorizontalAlignment="Center"/>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="LinkText" Property="Foreground" Value="{DynamicResource ButtonHover}"/>
<Setter TargetName="LinkText" Property="Foreground" Value="{DynamicResource ButtonHoverColor}"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="LinkText" Property="Foreground" Value="{DynamicResource ButtonPressed}"/>
<Setter TargetName="LinkText" Property="Foreground" Value="{DynamicResource ButtonPressedColor}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Button.Template>
</Button>
<Button x:Name="DeploymentApplyBtn" Style="{DynamicResource PrimaryButtonStyle}" Width="190" Height="44" HorizontalAlignment="Center" AutomationProperties.Name="Apply Changes">
<Button x:Name="DeploymentApplyBtn" Style="{DynamicResource PrimaryButtonStyle}" Width="200" Height="44" HorizontalAlignment="Center" AutomationProperties.Name="Apply Changes">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="&#xE73E;" FontFamily="Segoe Fluent Icons" FontSize="20" FontWeight="SemiBold" VerticalAlignment="Center"/>
<TextBlock Text="&#xE73E;" 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>
@@ -1022,18 +1006,18 @@
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Button x:Name="PreviousBtn" Grid.Column="0" Width="120" Height="36" Style="{DynamicResource SecondaryButtonStyle}" Visibility="Collapsed" Margin="10,0,0,0" AutomationProperties.Name="Back">
<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="&#xE72B;" FontFamily="Segoe Fluent Icons" FontSize="12" Margin="0,0,8,0" VerticalAlignment="Center"/>
<TextBlock Text="Back" VerticalAlignment="Center" FontSize="14" Margin="0,0,0,1"/>
<TextBlock Text="&#xE72B;" 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>
<StackPanel Grid.Column="2" Orientation="Horizontal" HorizontalAlignment="Right">
<Button x:Name="NextBtn" Width="120" Height="36" Margin="0,0,10,0" Style="{DynamicResource PrimaryButtonStyle}" AutomationProperties.Name="Next">
<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,1"/>
<TextBlock Text="&#xE72A;" FontFamily="Segoe Fluent Icons" FontSize="12" Margin="8,0,0,0" VerticalAlignment="Center"/>
<TextBlock Text="Next" VerticalAlignment="Center" FontSize="14" Margin="0,0,0,3"/>
<TextBlock Text="&#xE72A;" FontFamily="{DynamicResource AppIconFontFamily}" FontSize="12" Margin="8,0,0,0" VerticalAlignment="Center"/>
</StackPanel>
</Button>
</StackPanel>
@@ -12,7 +12,7 @@
Topmost="False"
ShowInTaskbar="False">
<Border BorderBrush="{DynamicResource BorderColor}"
<Border BorderBrush="{DynamicResource AppBorderColor}"
BorderThickness="1"
CornerRadius="8"
Background="{DynamicResource CardBgColor}"
@@ -33,14 +33,10 @@
</Grid.RowDefinitions>
<!-- Title Bar -->
<Grid Grid.Row="0" x:Name="TitleBar" Height="40" Background="Transparent">
<Grid Grid.Row="0" x:Name="TitleBar" Style="{DynamicResource ModalTitleBarStyle}">
<TextBlock x:Name="TitleText"
Text="Message"
Foreground="{DynamicResource FgColor}"
FontSize="16"
FontWeight="SemiBold"
VerticalAlignment="Center"
Margin="16,0,0,0"/>
Style="{DynamicResource ModalTitleTextStyle}"/>
</Grid>
<!-- Message Content -->
@@ -53,9 +49,9 @@
<!-- Icon -->
<TextBlock x:Name="IconText"
Grid.Column="0"
FontFamily="Segoe Fluent Icons"
FontFamily="{DynamicResource AppIconFontFamily}"
FontSize="24"
Foreground="{DynamicResource FgColor}"
Foreground="{DynamicResource AppFgColor}"
VerticalAlignment="Center"
Margin="4,0,14,0"
Visibility="Collapsed"/>
@@ -67,7 +63,7 @@
TextWrapping="Wrap"
FontSize="14"
LineHeight="20"
Foreground="{DynamicResource FgColor}"
Foreground="{DynamicResource AppFgColor}"
VerticalAlignment="Center"
Margin="0,0,20,0"/>
</ScrollViewer>
@@ -75,24 +71,27 @@
<!-- Button Panel -->
<Border Grid.Row="2"
Background="{DynamicResource BgColor}"
BorderBrush="{DynamicResource BorderColor}"
Background="{DynamicResource AppBgColor}"
BorderBrush="{DynamicResource AppBorderColor}"
BorderThickness="0,1,0,0"
Padding="16,12"
CornerRadius="0,0,8,8">
<StackPanel x:Name="ButtonPanel"
Orientation="Horizontal"
HorizontalAlignment="Right">
<Button x:Name="Button1"
Content="OK"
Height="32" MinWidth="80" Margin="4,0"
Style="{DynamicResource PrimaryButtonStyle}"/>
<Button x:Name="Button2"
Content="Cancel"
Height="32" MinWidth="80" Margin="4,0"
Style="{DynamicResource SecondaryButtonStyle}"
<Grid x:Name="ButtonPanel">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Button x:Name="Button1"
Grid.Column="0"
Content="OK"
Style="{DynamicResource ModalPrimaryStretchedButtonStyle}"/>
<Button x:Name="Button2"
Grid.Column="1"
Content="Cancel"
Style="{DynamicResource ModalSecondaryStretchedButtonStyle}"
Visibility="Collapsed"/>
</StackPanel>
</Grid>
</Border>
</Grid>
</Border>
+29 -29
View File
@@ -14,8 +14,8 @@
<Window.Resources>
<Style x:Key="RestoreOptionTileStyle" TargetType="Button">
<Setter Property="Background" Value="{DynamicResource SecondaryButtonBg}"/>
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
<Setter Property="Background" Value="{DynamicResource SecondaryButtonBgColor}"/>
<Setter Property="Foreground" Value="{DynamicResource AppFgColor}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderColor}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="12,10"/>
@@ -36,16 +36,16 @@
</Setter>
<Style.Triggers>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Background" Value="{DynamicResource SecondaryButtonDisabled}"/>
<Setter Property="BorderBrush" Value="{DynamicResource BorderColor}"/>
<Setter Property="Foreground" Value="{DynamicResource SecondaryButtonTextDisabled}"/>
<Setter Property="Background" Value="{DynamicResource SecondaryButtonDisabledColor}"/>
<Setter Property="BorderBrush" Value="{DynamicResource AppBorderColor}"/>
<Setter Property="Foreground" Value="{DynamicResource SecondaryButtonTextDisabledColor}"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="{DynamicResource SecondaryButtonHover}"/>
<Setter Property="Background" Value="{DynamicResource SecondaryButtonHoverColor}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderColor}"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Background" Value="{DynamicResource SecondaryButtonPressed}"/>
<Setter Property="Background" Value="{DynamicResource SecondaryButtonPressedColor}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderColor}"/>
</Trigger>
</Style.Triggers>
@@ -84,19 +84,19 @@
<Button.Style>
<Style TargetType="Button">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
<Setter Property="Foreground" Value="{DynamicResource AppFgColor}"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="{DynamicResource CloseHover}"/>
<Setter Property="Background" Value="{DynamicResource TitleBarCloseHoverColor}"/>
</Trigger>
</Style.Triggers>
</Style>
</Button.Style>
<TextBlock Text="&#xE8BB;" FontFamily="Segoe Fluent Icons" FontSize="10"/>
<TextBlock Text="&#xE8BB;" FontFamily="{DynamicResource AppIconFontFamily}" FontSize="10"/>
</Button>
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto" Margin="0">
<Grid Margin="20,4,20,18">
<Grid Margin="20,4,20,16">
<TabControl x:Name="RestoreModeTabs"
SelectedIndex="0"
@@ -121,7 +121,7 @@
TextWrapping="Wrap"
FontSize="14"
LineHeight="20"
Foreground="{DynamicResource FgColor}"
Foreground="{DynamicResource AppFgColor}"
Text="Choose what changes you want to restore."/>
<Button x:Name="ChooseRegistryBtn"
@@ -135,7 +135,7 @@
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0"
Text="&#xEF58;"
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="&#xE8FC;"
FontFamily="Segoe Fluent Icons"
FontFamily="{DynamicResource AppIconFontFamily}"
FontSize="24"
VerticalAlignment="Center"
Margin="14,0,14,0"/>
@@ -203,14 +203,14 @@
TextWrapping="Wrap"
FontSize="14"
LineHeight="20"
Foreground="{DynamicResource FgColor}"
Text="This will restore any system registry changes made by Win11Debloat to their previous state. You can review the changes after selecting a backup file. Apps will need to be reinstalled manually."/>
Foreground="{DynamicResource AppFgColor}"
Text="This will restore any system registry changes made by Win11Debloat to the previous state. You can review the changes after selecting a backup file. Removed applications will need to be reinstalled manually."/>
<TextBlock Grid.Row="1"
Margin="0,12,0,2"
TextWrapping="Wrap"
FontSize="13"
Foreground="{DynamicResource FgColor}"
Foreground="{DynamicResource AppFgColor}"
Opacity="0.75"
Text="Warning: Only use backup files generated by Win11Debloat."/>
</Grid>
@@ -247,7 +247,7 @@
<TextBlock x:Name="BackupTargetText" Grid.Row="2" Grid.Column="1" Text="N/A" Style="{DynamicResource ModalInfoValueTextStyle}" Margin="0"/>
</Grid>
<Border Grid.Row="1" Height="1" Background="{DynamicResource BorderColor}" Margin="0,0,0,12"/>
<Border Grid.Row="1" Height="1" Background="{DynamicResource AppBorderColor}" Margin="0,0,0,12"/>
<Grid x:Name="OverviewFeaturesSection" Grid.Row="2">
<Grid.RowDefinitions>
@@ -258,7 +258,7 @@
<TextBlock Grid.Row="0"
Text="The following changes will be reverted:"
FontSize="13"
Foreground="{DynamicResource FgColor}"
Foreground="{DynamicResource AppFgColor}"
FontWeight="SemiBold"
Margin="0,0,0,8"/>
@@ -270,7 +270,7 @@
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding DisplayText}" FontSize="12" Foreground="{DynamicResource FgColor}" TextWrapping="Wrap" Margin="0,0,0,6"/>
<TextBlock Text="{Binding DisplayText}" FontSize="12" Foreground="{DynamicResource AppFgColor}" TextWrapping="Wrap" Margin="0,0,0,6"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
@@ -279,12 +279,12 @@
<TextBlock x:Name="OverviewSummaryText"
Grid.Row="2"
FontSize="13"
Foreground="{DynamicResource FgColor}"
Foreground="{DynamicResource AppFgColor}"
TextWrapping="Wrap"
Visibility="Collapsed"
Text="This will restore the Start Menu pinned apps layout for the current user."/>
<Border x:Name="ReappliedSeparator" Grid.Row="3" Height="1" Background="{DynamicResource BorderColor}" Margin="0,12,0,12" Visibility="Collapsed"/>
<Border x:Name="ReappliedSeparator" Grid.Row="3" Height="1" Background="{DynamicResource AppBorderColor}" Margin="0,12,0,12" Visibility="Collapsed"/>
<Grid x:Name="ReappliedPanel" Grid.Row="4" Visibility="Collapsed">
<Grid.RowDefinitions>
@@ -295,7 +295,7 @@
<TextBlock Grid.Row="0"
Text="The following changes will be re-applied:"
FontSize="13"
Foreground="{DynamicResource FgColor}"
Foreground="{DynamicResource AppFgColor}"
FontWeight="SemiBold"
Margin="0,0,0,8"/>
@@ -307,13 +307,13 @@
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding DisplayText}" FontSize="12" Foreground="{DynamicResource FgColor}" TextWrapping="Wrap" Margin="0,0,0,6"/>
<TextBlock Text="{Binding DisplayText}" FontSize="12" Foreground="{DynamicResource AppFgColor}" TextWrapping="Wrap" Margin="0,0,0,6"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
<Border x:Name="NonRevertibleSeparator" Grid.Row="5" Height="1" Background="{DynamicResource BorderColor}" Margin="0,12,0,12" Visibility="Collapsed"/>
<Border x:Name="NonRevertibleSeparator" Grid.Row="5" Height="1" Background="{DynamicResource AppBorderColor}" Margin="0,12,0,12" Visibility="Collapsed"/>
<Grid x:Name="NonRevertiblePanel" Grid.Row="6" Visibility="Collapsed">
<Grid.RowDefinitions>
@@ -325,7 +325,7 @@
<TextBlock Grid.Row="0"
Text="The following changes won't be reverted:"
FontSize="13"
Foreground="{DynamicResource FgColor}"
Foreground="{DynamicResource AppFgColor}"
FontWeight="SemiBold"
Margin="0,0,0,8"/>
@@ -337,7 +337,7 @@
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding DisplayText}" FontSize="12" Foreground="{DynamicResource FgColor}" Opacity="0.85" TextWrapping="Wrap" Margin="0,0,0,6"/>
<TextBlock Text="{Binding DisplayText}" FontSize="12" Foreground="{DynamicResource AppFgColor}" Opacity="0.85" TextWrapping="Wrap" Margin="0,0,0,6"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
@@ -373,7 +373,7 @@
FontSize="14"
LineHeight="20"
Margin="0,0,0,12"
Foreground="{DynamicResource FgColor}"
Foreground="{DynamicResource AppFgColor}"
Text="This will restore the Start Menu pinned apps layout for the selected user(s) using a backup that is automatically created by Win11Debloat. Manually created backups can also be used."/>
<ComboBox x:Name="StartMenuScopeCombo"
@@ -392,7 +392,7 @@
IsChecked="True"
FontSize="12"
Margin="0,10,0,0"
Foreground="{DynamicResource FgColor}"
Foreground="{DynamicResource AppFgColor}"
Opacity="0.85"/>
</Grid>
+109 -64
View File
@@ -3,9 +3,9 @@
<!-- Primary Button Style -->
<Style x:Key="PrimaryButtonStyle" TargetType="Button">
<Setter Property="Background" Value="{DynamicResource ButtonBg}"/>
<Setter Property="Background" Value="{DynamicResource ButtonBgColor}"/>
<Setter Property="Foreground" Value="white"/>
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBg}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBgColor}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="0"/>
<Setter Property="FontSize" Value="14"/>
@@ -25,25 +25,25 @@
</Setter>
<Style.Triggers>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Background" Value="{DynamicResource ButtonDisabled}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ButtonDisabled}"/>
<Setter Property="Foreground" Value="{DynamicResource ButtonTextDisabled}"/>
<Setter Property="Background" Value="{DynamicResource ButtonDisabledColor}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ButtonDisabledColor}"/>
<Setter Property="Foreground" Value="{DynamicResource ButtonTextDisabledColor}"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="{DynamicResource ButtonHover}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ButtonHover}"/>
<Setter Property="Background" Value="{DynamicResource ButtonHoverColor}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ButtonHoverColor}"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Background" Value="{DynamicResource ButtonPressed}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ButtonPressed}"/>
<Setter Property="Background" Value="{DynamicResource ButtonPressedColor}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ButtonPressedColor}"/>
</Trigger>
</Style.Triggers>
</Style>
<!-- Secondary Button Style -->
<Style x:Key="SecondaryButtonStyle" TargetType="Button">
<Setter Property="Background" Value="{DynamicResource SecondaryButtonBg}"/>
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
<Setter Property="Background" Value="{DynamicResource SecondaryButtonBgColor}"/>
<Setter Property="Foreground" Value="{DynamicResource AppFgColor}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderColor}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="0"/>
@@ -64,16 +64,16 @@
</Setter>
<Style.Triggers>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Background" Value="{DynamicResource SecondaryButtonDisabled}"/>
<Setter Property="BorderBrush" Value="{DynamicResource BorderColor}"/>
<Setter Property="Foreground" Value="{DynamicResource SecondaryButtonTextDisabled}"/>
<Setter Property="Background" Value="{DynamicResource SecondaryButtonDisabledColor}"/>
<Setter Property="BorderBrush" Value="{DynamicResource AppBorderColor}"/>
<Setter Property="Foreground" Value="{DynamicResource SecondaryButtonTextDisabledColor}"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="{DynamicResource SecondaryButtonHover}"/>
<Setter Property="Background" Value="{DynamicResource SecondaryButtonHoverColor}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderColor}"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Background" Value="{DynamicResource SecondaryButtonPressed}"/>
<Setter Property="Background" Value="{DynamicResource SecondaryButtonPressedColor}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderColor}"/>
</Trigger>
</Style.Triggers>
@@ -81,11 +81,11 @@
<!-- Hyperlink Style -->
<Style x:Key="HyperlinkStyle" TargetType="TextBlock">
<Setter Property="Foreground" Value="{DynamicResource ButtonBg}"/>
<Setter Property="Foreground" Value="{DynamicResource ButtonBgColor}"/>
<Setter Property="Cursor" Value="Hand"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Foreground" Value="{DynamicResource ButtonHover}"/>
<Setter Property="Foreground" Value="{DynamicResource ButtonHoverColor}"/>
</Trigger>
</Style.Triggers>
</Style>
@@ -93,7 +93,7 @@
<!-- ProgressBar Style -->
<Style x:Key="ApplyProgressBarStyle" TargetType="ProgressBar">
<Setter Property="Background" Value="{DynamicResource ButtonBorderColor}"/>
<Setter Property="Foreground" Value="{DynamicResource ButtonBg}"/>
<Setter Property="Foreground" Value="{DynamicResource ButtonBgColor}"/>
<Setter Property="Height" Value="6"/>
<Setter Property="Template">
<Setter.Value>
@@ -109,9 +109,9 @@
<!-- Modal Title Style -->
<Style x:Key="ModalTitleStyle" TargetType="TextBlock">
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="FontSize" Value="20"/>
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
<Setter Property="Foreground" Value="{DynamicResource AppFgColor}"/>
<Setter Property="HorizontalAlignment" Value="Center"/>
<Setter Property="Margin" Value="0,0,0,8"/>
</Style>
@@ -119,7 +119,7 @@
<!-- Modal Subtext Style -->
<Style x:Key="ModalSubtextStyle" TargetType="TextBlock">
<Setter Property="FontSize" Value="13"/>
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
<Setter Property="Foreground" Value="{DynamicResource AppFgColor}"/>
<Setter Property="Opacity" Value="0.8"/>
<Setter Property="HorizontalAlignment" Value="Center"/>
<Setter Property="TextAlignment" Value="Center"/>
@@ -127,7 +127,7 @@
<!-- Shared modal window shell styles -->
<Style x:Key="ModalCardBorderStyle" TargetType="Border">
<Setter Property="BorderBrush" Value="{DynamicResource BorderColor}"/>
<Setter Property="BorderBrush" Value="{DynamicResource AppBorderColor}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="CornerRadius" Value="8"/>
<Setter Property="Background" Value="{DynamicResource CardBgColor}"/>
@@ -149,16 +149,16 @@
</Style>
<Style x:Key="ModalTitleTextStyle" TargetType="TextBlock">
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
<Setter Property="FontSize" Value="16"/>
<Setter Property="Foreground" Value="{DynamicResource AppFgColor}"/>
<Setter Property="FontSize" Value="20"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="Margin" Value="20,0,0,0"/>
<Setter Property="Margin" Value="16,0,0,0"/>
</Style>
<Style x:Key="ModalFooterBorderStyle" TargetType="Border">
<Setter Property="Background" Value="{DynamicResource BgColor}"/>
<Setter Property="BorderBrush" Value="{DynamicResource BorderColor}"/>
<Setter Property="Background" Value="{DynamicResource AppBgColor}"/>
<Setter Property="BorderBrush" Value="{DynamicResource AppBorderColor}"/>
<Setter Property="BorderThickness" Value="0,1,0,0"/>
<Setter Property="Padding" Value="16,12"/>
<Setter Property="CornerRadius" Value="0,0,8,8"/>
@@ -195,20 +195,20 @@
<Style x:Key="ModalInfoLabelTextStyle" TargetType="TextBlock">
<Setter Property="FontSize" Value="13"/>
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
<Setter Property="Foreground" Value="{DynamicResource AppFgColor}"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Margin" Value="0,0,16,8"/>
</Style>
<Style x:Key="ModalInfoValueTextStyle" TargetType="TextBlock">
<Setter Property="FontSize" Value="13"/>
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
<Setter Property="Foreground" Value="{DynamicResource AppFgColor}"/>
<Setter Property="Margin" Value="0,0,0,8"/>
</Style>
<!-- Shared ComboBox style used across windows -->
<Style TargetType="ComboBoxItem">
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
<Setter Property="Foreground" Value="{DynamicResource AppFgColor}"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Padding" Value="10,8"/>
<Setter Property="Margin" Value="0,0,0,4"/>
@@ -223,14 +223,15 @@
Padding="{TemplateBinding Padding}"
CornerRadius="4">
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Margin="0,0,0,1"/>
</Border>
<Border x:Name="AccentLine"
Width="3"
Height="15"
HorizontalAlignment="Left"
VerticalAlignment="Stretch"
Background="{DynamicResource ButtonBg}"
Background="{DynamicResource ButtonBgColor}"
CornerRadius="1.5"
Margin="0"
Visibility="Collapsed"/>
@@ -238,10 +239,10 @@
<ControlTemplate.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="AccentLine" Property="Visibility" Value="Visible"/>
<Setter TargetName="ItemBorder" Property="Background" Value="{DynamicResource ComboItemSelectedColor}"/>
<Setter TargetName="ItemBorder" Property="Background" Value="{DynamicResource ComboBoxItemSelectedColor}"/>
</Trigger>
<Trigger Property="IsHighlighted" Value="True">
<Setter TargetName="ItemBorder" Property="Background" Value="{DynamicResource ComboItemHoverColor}"/>
<Setter TargetName="ItemBorder" Property="Background" Value="{DynamicResource ComboBoxItemHoverColor}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
@@ -250,17 +251,18 @@
</Style>
<Style TargetType="ComboBox">
<Setter Property="Background" Value="{DynamicResource ComboBgColor}"/>
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
<Setter Property="BorderBrush" Value="{DynamicResource BorderColor}"/>
<Setter Property="Background" Value="{DynamicResource ComboBoxBgColor}"/>
<Setter Property="Foreground" Value="{DynamicResource AppFgColor}"/>
<Setter Property="BorderBrush" Value="{DynamicResource AppBorderColor}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="FontSize" Value="13"/>
<Setter Property="Margin" Value="0,4,0,12"/>
<Setter Property="MinHeight" Value="33"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ComboBox">
<Grid>
<Border x:Name="ClosedAccentLine" Width="3" Height="18" HorizontalAlignment="Left" VerticalAlignment="Stretch" Background="{DynamicResource ButtonBg}" CornerRadius="1.5" Panel.ZIndex="2"/>
<Border x:Name="ClosedAccentLine" Width="3" Height="18" HorizontalAlignment="Left" VerticalAlignment="Stretch" Background="{DynamicResource ButtonBgColor}" CornerRadius="1.5" Panel.ZIndex="2"/>
<ToggleButton x:Name="ToggleButton" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Focusable="False" IsChecked="{Binding Path=IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" ClickMode="Press">
<ToggleButton.Style>
<Style TargetType="ToggleButton">
@@ -270,12 +272,12 @@
<Border x:Name="Border" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="4">
<TextBlock x:Name="Arrow"
Text="&#xE70D;"
FontFamily="Segoe Fluent Icons"
FontFamily="{DynamicResource AppIconFontFamily}"
FontSize="10"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Margin="0,0,8,0"
Foreground="{DynamicResource FgColor}"
Foreground="{DynamicResource AppFgColor}"
RenderTransformOrigin="0.5,0.5">
<TextBlock.RenderTransform>
<RotateTransform Angle="0"/>
@@ -284,7 +286,7 @@
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Border" Property="Background" Value="{DynamicResource ComboHoverColor}"/>
<Setter TargetName="Border" Property="Background" Value="{DynamicResource ComboBoxHoverColor}"/>
</Trigger>
<Trigger Property="IsChecked" Value="True">
<Trigger.EnterActions>
@@ -319,7 +321,7 @@
</ToggleButton>
<ContentPresenter x:Name="ContentPresenter"
IsHitTestVisible="False"
Margin="10,0,20,0"
Margin="10,0,20,2"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Content="{TemplateBinding SelectionBoxItem}"
@@ -337,8 +339,8 @@
HorizontalOffset="0">
<Grid x:Name="DropDown" MinWidth="{TemplateBinding ActualWidth}" MaxHeight="{TemplateBinding MaxDropDownHeight}" Margin="12">
<Border x:Name="DropDownBorder"
Background="{DynamicResource ComboItemBgColor}"
BorderBrush="{DynamicResource BorderColor}"
Background="{DynamicResource ComboBoxItemBgColor}"
BorderBrush="{DynamicResource AppBorderColor}"
BorderThickness="1"
CornerRadius="4"
Padding="5,4,5,1">
@@ -363,9 +365,9 @@
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="ClosedAccentLine" Property="Visibility" Value="Collapsed"/>
<Setter Property="Background" Value="{DynamicResource ButtonDisabled}"/>
<Setter Property="BorderBrush" Value="{DynamicResource BorderColor}"/>
<Setter Property="Foreground" Value="{DynamicResource ButtonTextDisabled}"/>
<Setter Property="Background" Value="{DynamicResource ButtonDisabledColor}"/>
<Setter Property="BorderBrush" Value="{DynamicResource AppBorderColor}"/>
<Setter Property="Foreground" Value="{DynamicResource ButtonTextDisabledColor}"/>
<Setter Property="Opacity" Value="0.6"/>
</Trigger>
</ControlTemplate.Triggers>
@@ -376,8 +378,9 @@
<!-- Base CheckBox style used across windows -->
<Style TargetType="CheckBox">
<Setter Property="Foreground" Value="{DynamicResource FgColor}"/>
<Setter Property="Foreground" Value="{DynamicResource AppFgColor}"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="FontSize" Value="13"/>
<Setter Property="Padding" Value="4,2"/>
<Setter Property="Template">
<Setter.Value>
@@ -388,16 +391,16 @@
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Border x:Name="CheckBoxBorder" Grid.Column="0" Width="18" Height="18" Background="{DynamicResource CheckBoxBgColor}" BorderBrush="{DynamicResource CheckBoxBorderColor}" BorderThickness="1" CornerRadius="4" Margin="0,0,8,0">
<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="&#xE73E;" FontFamily="Segoe Fluent Icons" FontSize="12" Foreground="{DynamicResource ButtonBg}" HorizontalAlignment="Center" VerticalAlignment="Center" Opacity="0">
<TextBlock x:Name="CheckMark" Text="&#xE73E;" 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="&#xE738;" FontFamily="Segoe Fluent Icons" FontSize="11" Foreground="{DynamicResource ButtonBg}" HorizontalAlignment="Center" VerticalAlignment="Center" Opacity="0" Margin="1,1,0,0">
<TextBlock x:Name="IndeterminateMark" Text="&#xe9ae;" 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,16"/>
<RectangleGeometry x:Name="IndeterminateMarkClip" Rect="0,0,0,12"/>
</TextBlock.Clip>
</TextBlock>
</Grid>
@@ -410,8 +413,8 @@
<Setter TargetName="CheckBoxBorder" Property="Background" Value="{DynamicResource CheckBoxHoverColor}"/>
</Trigger>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="CheckBoxBorder" Property="Background" Value="{DynamicResource ButtonBg}"/>
<Setter TargetName="CheckBoxBorder" Property="BorderBrush" Value="{DynamicResource ButtonBg}"/>
<Setter TargetName="CheckBoxBorder" Property="Background" Value="{DynamicResource ButtonBgColor}"/>
<Setter TargetName="CheckBoxBorder" Property="BorderBrush" Value="{DynamicResource ButtonBgColor}"/>
<Setter TargetName="CheckMark" Property="Foreground" Value="White"/>
<Setter TargetName="CheckMark" Property="Opacity" Value="1"/>
<Setter TargetName="IndeterminateMark" Property="Opacity" Value="0"/>
@@ -433,9 +436,8 @@
</Trigger.ExitActions>
</Trigger>
<Trigger Property="IsChecked" Value="{x:Null}">
<Setter TargetName="CheckBoxBorder" Property="Background" Value="{DynamicResource ButtonBg}"/>
<Setter TargetName="CheckBoxBorder" Property="BorderBrush" Value="{DynamicResource ButtonBg}"/>
<Setter TargetName="CheckBoxBorder" Property="Opacity" Value="0.8"/>
<Setter TargetName="CheckBoxBorder" Property="Background" Value="{DynamicResource ButtonBgColor}"/>
<Setter TargetName="CheckBoxBorder" Property="BorderBrush" Value="{DynamicResource ButtonBgColor}"/>
<Setter TargetName="IndeterminateMark" Property="Foreground" Value="White"/>
<Setter TargetName="IndeterminateMark" Property="Opacity" Value="1"/>
<Setter TargetName="CheckMark" Property="Opacity" Value="0"/>
@@ -461,8 +463,8 @@
<Setter TargetName="IndeterminateMark" Property="Opacity" Value="0"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="CheckBoxBorder" Property="Background" Value="{DynamicResource ButtonDisabled}"/>
<Setter TargetName="CheckBoxBorder" Property="BorderBrush" Value="{DynamicResource BorderColor}"/>
<Setter TargetName="CheckBoxBorder" Property="Background" Value="{DynamicResource ButtonDisabledColor}"/>
<Setter TargetName="CheckBoxBorder" Property="BorderBrush" Value="{DynamicResource AppBorderColor}"/>
<Setter Property="Opacity" Value="0.4"/>
</Trigger>
<MultiTrigger>
@@ -470,16 +472,16 @@
<Condition Property="IsMouseOver" Value="True"/>
<Condition Property="IsChecked" Value="True"/>
</MultiTrigger.Conditions>
<Setter TargetName="CheckBoxBorder" Property="Background" Value="{DynamicResource ButtonHover}"/>
<Setter TargetName="CheckBoxBorder" Property="BorderBrush" Value="{DynamicResource ButtonHover}"/>
<Setter TargetName="CheckBoxBorder" Property="Background" Value="{DynamicResource ButtonHoverColor}"/>
<Setter TargetName="CheckBoxBorder" Property="BorderBrush" Value="{DynamicResource ButtonHoverColor}"/>
</MultiTrigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsMouseOver" Value="True"/>
<Condition Property="IsChecked" Value="{x:Null}"/>
</MultiTrigger.Conditions>
<Setter TargetName="CheckBoxBorder" Property="Background" Value="{DynamicResource ButtonHover}"/>
<Setter TargetName="CheckBoxBorder" Property="BorderBrush" Value="{DynamicResource ButtonHover}"/>
<Setter TargetName="CheckBoxBorder" Property="Background" Value="{DynamicResource ButtonHoverColor}"/>
<Setter TargetName="CheckBoxBorder" Property="BorderBrush" Value="{DynamicResource ButtonHoverColor}"/>
<Setter TargetName="CheckBoxBorder" Property="Opacity" Value="0.8"/>
</MultiTrigger>
</ControlTemplate.Triggers>
@@ -543,4 +545,47 @@
</Setter>
</Style>
<!-- TextBox outer border: side/top borders in side color, background with hover/focus states -->
<Style x:Key="TextBoxBorderStyle" TargetType="Border">
<Setter Property="Background" Value="{DynamicResource TextBoxBgColor}"/>
<Setter Property="BorderBrush" Value="{DynamicResource TextBoxSideBorderColor}"/>
<Setter Property="BorderThickness" Value="1,1,1,0"/>
<Setter Property="CornerRadius" Value="4"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="{DynamicResource TextBoxHoverColor}"/>
</Trigger>
<Trigger Property="IsKeyboardFocusWithin" Value="True">
<Setter Property="Background" Value="{DynamicResource TextBoxFocusColor}"/>
</Trigger>
</Style.Triggers>
</Style>
<!-- TextBox inner border: bottom accent line that thickens and changes color on focus -->
<Style x:Key="TextBoxBottomBorderStyle" TargetType="Border">
<Setter Property="BorderBrush" Value="{DynamicResource TextBoxBorderColor}"/>
<Setter Property="BorderThickness" Value="0,0,0,1"/>
<Setter Property="Padding" Value="8,6"/>
<Setter Property="CornerRadius" Value="0,0,4,4"/>
<Style.Triggers>
<Trigger Property="IsKeyboardFocusWithin" Value="True">
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBgColor}"/>
<Setter Property="BorderThickness" Value="0,0,0,2"/>
<Setter Property="Padding" Value="8,6,8,5"/>
</Trigger>
</Style.Triggers>
</Style>
<!-- TextBox input (transparent, used inside TextBoxBorderStyle) -->
<Style x:Key="TextBoxInputStyle" TargetType="TextBox">
<Setter Property="CaretBrush" Value="{DynamicResource AppFgColor}"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Foreground" Value="{DynamicResource AppFgColor}"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="FontSize" Value="13"/>
<Setter Property="Margin" Value="1,0,0,1"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="Padding" Value="0"/>
</Style>
</ResourceDictionary>
-57
View File
@@ -1,57 +0,0 @@
# Forcefully removes Microsoft Edge using its uninstaller
# Credit: Based on work from loadstring1 & ave9858
function ForceRemoveEdge {
Write-Host "> Forcefully uninstalling Microsoft Edge..."
$regView = [Microsoft.Win32.RegistryView]::Registry32
$hklm = [Microsoft.Win32.RegistryKey]::OpenBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine, $regView)
$hklm.CreateSubKey('SOFTWARE\Microsoft\EdgeUpdateDev').SetValue('AllowUninstall', '')
# Create stub (This somehow allows uninstalling Edge)
$edgeStub = "$env:SystemRoot\SystemApps\Microsoft.MicrosoftEdge_8wekyb3d8bbwe"
New-Item $edgeStub -ItemType Directory | Out-Null
New-Item "$edgeStub\MicrosoftEdge.exe" | Out-Null
# Remove edge
$uninstallRegKey = $hklm.OpenSubKey('SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Microsoft Edge')
if ($null -ne $uninstallRegKey) {
Write-Host "Running uninstaller..."
$uninstallString = $uninstallRegKey.GetValue('UninstallString') + ' --force-uninstall'
Invoke-NonBlocking -ScriptBlock {
param($cmd)
Start-Process cmd.exe "/c $cmd" -WindowStyle Hidden -Wait
} -ArgumentList $uninstallString
Write-Host "Removing leftover files..."
$edgePaths = @(
"$env:ProgramData\Microsoft\Windows\Start Menu\Programs\Microsoft Edge.lnk",
"$env:APPDATA\Microsoft\Internet Explorer\Quick Launch\Microsoft Edge.lnk",
"$env:APPDATA\Microsoft\Internet Explorer\Quick Launch\User Pinned\TaskBar\Microsoft Edge.lnk",
"$env:APPDATA\Microsoft\Internet Explorer\Quick Launch\User Pinned\TaskBar\Tombstones\Microsoft Edge.lnk",
"$env:PUBLIC\Desktop\Microsoft Edge.lnk",
"$env:USERPROFILE\Desktop\Microsoft Edge.lnk",
"$edgeStub"
)
foreach ($path in $edgePaths) {
if (Test-Path -Path $path) {
Remove-Item -Path $path -Force -Recurse -ErrorAction SilentlyContinue
Write-Host " Removed $path" -ForegroundColor DarkGray
}
}
Write-Host "Cleaning up registry..."
# Remove MS Edge from autostart
reg delete "HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run" /v "MicrosoftEdgeAutoLaunch_A9F6DCE4ABADF4F51CF45CD7129E3C6C" /f *>$null
reg delete "HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run" /v "Microsoft Edge Update" /f *>$null
reg delete "HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\Run" /v "MicrosoftEdgeAutoLaunch_A9F6DCE4ABADF4F51CF45CD7129E3C6C" /f *>$null
reg delete "HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\Run" /v "Microsoft Edge Update" /f *>$null
Write-Host "Microsoft Edge was uninstalled"
}
else {
Write-Host "Unable to forcefully uninstall Microsoft Edge, uninstaller could not be found" -ForegroundColor Red
}
}
@@ -0,0 +1,117 @@
<#
.SYNOPSIS
Returns a list of installed apps from winget as structured objects.
.DESCRIPTION
Runs `winget list` and parses the output into PSCustomObject arrays.
Use -NonBlocking to keep the UI responsive in GUI mode; otherwise
runs synchronously with an optional timeout.
.PARAMETER TimeOut
Maximum seconds to wait for winget to complete. Default is 10.
.PARAMETER NonBlocking
When set, runs via Invoke-NonBlocking so the GUI thread stays responsive.
.OUTPUTS
PSCustomObject[] with Name and Id properties. Returns $null on
failure, or an empty array when winget succeeds but lists no apps.
#>
function Get-WingetInstalledApps {
param (
[int]$TimeOut = 10,
[switch]$NonBlocking
)
if (-not $script:WingetInstalled) { return $null }
$fetchBlock = {
param($timeOut)
$job = Start-Job {
$rawOutput = $null
try {
$originalEncoding = [Console]::OutputEncoding
[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new()
try {
$rawOutput = winget list --accept-source-agreements --disable-interactivity
}
finally {
[Console]::OutputEncoding = $originalEncoding
}
return $rawOutput
}
catch {
return $null
}
}
$done = $job | Wait-Job -Timeout $timeOut
if ($done) {
$result = Receive-Job -Job $job
Remove-Job -Job $job -ErrorAction SilentlyContinue
if (-not $result) { return $null }
# winget list outputs:
# [progress line] / [blank] / header / --- separator / data rows
$textOutput = $result -join "`n"
$lines = $textOutput -split "`r`n|`n"
# Find the separator line to know where data starts
$dataStart = -1
for ($i = 0; $i -lt $lines.Count; $i++) {
if ($lines[$i] -match '^-{3,}') {
$dataStart = $i + 1
break
}
}
# A missing table separator means the output is malformed or empty
if ($dataStart -lt 0) {
return $null
}
if ($dataStart -ge $lines.Count) {
return ,@()
}
$apps = [System.Collections.Generic.List[object]]::new()
for ($i = $dataStart; $i -lt $lines.Count; $i++) {
$line = $lines[$i]
if ($line.Trim() -eq '') { continue }
try {
# Split on 2+ spaces; extract Name and Id columns.
$fields = [regex]::Split($line.Trim(), '\s{2,}')
if ($fields.Count -lt 2) { continue }
$name = $fields[0].Trim()
$id = $fields[1].Trim()
if (-not $id) { continue }
$null = $apps.Add([PSCustomObject]@{
Name = $name
Id = $id
})
}
catch {
# Skip lines that can't be parsed
}
}
return ,@($apps)
}
Remove-Job -Job $job -Force -ErrorAction SilentlyContinue
return $null
}
if ($NonBlocking) {
return Invoke-NonBlocking -ScriptBlock $fetchBlock -ArgumentList $TimeOut
}
else {
return & $fetchBlock $TimeOut
}
}
@@ -1,31 +0,0 @@
# Run winget list and return installed apps.
# Use -NonBlocking to keep the UI responsive (GUI mode) via Invoke-NonBlocking.
function GetInstalledAppsViaWinget {
param (
[int]$TimeOut = 10,
[switch]$NonBlocking
)
if (-not $script:WingetInstalled) { return $null }
$fetchBlock = {
param($timeOut)
$job = Start-Job { return winget list --accept-source-agreements --disable-interactivity }
$done = $job | Wait-Job -Timeout $timeOut
if ($done) {
$result = Receive-Job -Job $job
Remove-Job -Job $job -ErrorAction SilentlyContinue
return $result
}
Remove-Job -Job $job -Force -ErrorAction SilentlyContinue
return $null
}
if ($NonBlocking) {
return Invoke-NonBlocking -ScriptBlock $fetchBlock -ArgumentList $TimeOut
}
else {
return & $fetchBlock $TimeOut
}
}
@@ -0,0 +1,146 @@
<#
.SYNOPSIS
Forcefully uninstalls Microsoft Edge and removes its leftover shortcuts and autostart entries.
.OUTPUTS
System.Boolean. $true when Edge is uninstalled and cleanup succeeds; otherwise $false.
#>
function Invoke-ForceRemoveEdge {
if ($script:Params.ContainsKey("WhatIf")) {
Write-Host "[WhatIf] Forcefully uninstall Microsoft Edge" -ForegroundColor Cyan
return $true
}
try {
Write-Host "> Forcefully uninstalling Microsoft Edge..."
$regView = [Microsoft.Win32.RegistryView]::Registry32
$hklm = [Microsoft.Win32.RegistryKey]::OpenBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine, $regView)
$edgeUpdateKey = $hklm.CreateSubKey('SOFTWARE\Microsoft\EdgeUpdateDev')
$edgeUpdateKey.SetValue('AllowUninstall', '')
# Create stub (This somehow allows uninstalling Edge)
$edgeStub = "$env:SystemRoot\SystemApps\Microsoft.MicrosoftEdge_8wekyb3d8bbwe"
New-Item $edgeStub -ItemType Directory -Force -ErrorAction Stop | Out-Null
New-Item "$edgeStub\MicrosoftEdge.exe" -ItemType File -Force -ErrorAction Stop | Out-Null
# Remove edge
$uninstallRegKey = $hklm.OpenSubKey('SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Microsoft Edge')
if ($null -eq $uninstallRegKey) {
Write-Host "Unable to forcefully uninstall Microsoft Edge, uninstaller could not be found" -ForegroundColor Red
return $false
}
Write-Host "Running uninstaller..."
$uninstallString = $uninstallRegKey.GetValue('UninstallString') + ' --force-uninstall'
$exitCode = Invoke-NonBlocking -ScriptBlock {
param($cmd)
$process = Start-Process cmd.exe "/c $cmd" -WindowStyle Hidden -Wait -PassThru
return $process.ExitCode
} -ArgumentList $uninstallString
if ($exitCode -ne 0) {
Write-Warning "Microsoft Edge uninstaller failed with exit code $exitCode."
return $false
}
Write-Host "Removing leftover files..."
$cleanupSucceeded = $true
$edgePaths = @(
"$env:ProgramData\Microsoft\Windows\Start Menu\Programs\Microsoft Edge.lnk",
"$env:APPDATA\Microsoft\Internet Explorer\Quick Launch\Microsoft Edge.lnk",
"$env:APPDATA\Microsoft\Internet Explorer\Quick Launch\User Pinned\TaskBar\Microsoft Edge.lnk",
"$env:APPDATA\Microsoft\Internet Explorer\Quick Launch\User Pinned\TaskBar\Tombstones\Microsoft Edge.lnk",
"$env:PUBLIC\Desktop\Microsoft Edge.lnk",
"$env:USERPROFILE\Desktop\Microsoft Edge.lnk",
"$edgeStub"
)
foreach ($path in $edgePaths) {
if (Test-Path -Path $path) {
try {
Remove-Item -Path $path -Force -Recurse -ErrorAction Stop
Write-Host " Removed $path" -ForegroundColor DarkGray
}
catch {
Write-Warning "Failed to remove Edge leftover '$path': $($_.Exception.Message)"
$cleanupSucceeded = $false
}
}
}
Write-Host "Cleaning up registry..."
$registryCleanupSucceeded = $true
# Remove MS Edge from autostart. Missing values are already-clean state,
# while failures to inspect or remove an existing value are reported.
$autostartValues = @(
@{ Path = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Run'; Name = 'MicrosoftEdgeAutoLaunch_A9F6DCE4ABADF4F51CF45CD7129E3C6C' },
@{ Path = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Run'; Name = 'Microsoft Edge Update' },
@{ Path = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\Run'; Name = 'MicrosoftEdgeAutoLaunch_A9F6DCE4ABADF4F51CF45CD7129E3C6C' },
@{ Path = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\Run'; Name = 'Microsoft Edge Update' }
)
foreach ($autostartValue in $autostartValues) {
if (-not (Remove-EdgeAutostartValue -Path $autostartValue.Path -Name $autostartValue.Name)) {
$registryCleanupSucceeded = $false
}
}
if (-not $cleanupSucceeded -or -not $registryCleanupSucceeded) {
Write-Warning "Microsoft Edge was uninstalled, but some leftover files or autostart entries could not be removed."
return $false
}
Write-Host "Microsoft Edge was uninstalled"
return $true
}
catch {
Write-Warning "Failed to forcefully uninstall Microsoft Edge: $($_.Exception.Message)"
return $false
}
finally {
if ($edgeUpdateKey) { $edgeUpdateKey.Dispose() }
if ($uninstallRegKey) { $uninstallRegKey.Dispose() }
if ($hklm) { $hklm.Dispose() }
}
}
<#
.SYNOPSIS
Removes an Edge autostart registry value when it exists.
.OUTPUTS
System.Boolean. $true when the value is absent or removed; $false when inspection or removal fails.
#>
function Remove-EdgeAutostartValue {
param(
[Parameter(Mandatory)]
[string]$Path,
[Parameter(Mandatory)]
[string]$Name
)
try {
$properties = Get-ItemProperty -Path $Path -ErrorAction Stop
}
catch [System.Management.Automation.ItemNotFoundException] {
return $true
}
catch {
Write-Warning "Failed to inspect Edge autostart entry '$Path\$Name': $($_.Exception.Message)"
return $false
}
if (-not $properties.PSObject.Properties[$Name]) {
return $true
}
try {
Remove-ItemProperty -Path $Path -Name $Name -ErrorAction Stop
return $true
}
catch {
Write-Warning "Failed to remove Edge autostart entry '$Path\$Name': $($_.Exception.Message)"
return $false
}
}
+400
View File
@@ -0,0 +1,400 @@
<#
.SYNOPSIS
Removes one or more Windows app packages based on the target scope.
.DESCRIPTION
Iterates over the provided list of app identifiers and removes each one.
The removal method (winget vs. Appx cmdlets) is determined per-app from
Apps.json. A scheduled task is only created when the User or Sysprep
parameter was passed. After winget removal, the system is checked to
confirm whether the app is still installed before reporting an error.
Returns early if the CancelRequested flag is set.
.PARAMETER appsList
An array of app package identifiers to remove (e.g. 'Microsoft.BingNews').
.EXAMPLE
Remove-SelectedApps @('Microsoft.BingNews', 'Microsoft.BingWeather')
.EXAMPLE
Remove-SelectedApps -appsList (Generate-AppsList)
.OUTPUTS
System.Boolean. $true when all removals can be confirmed; otherwise $false.
#>
function Remove-SelectedApps {
param (
$appslist
)
if ($script:Params.ContainsKey("WhatIf")) {
foreach ($app in $appslist) {
Write-Host "[WhatIf] Remove App Package: $app" -ForegroundColor Cyan
}
return $true
}
$failuresBefore = $script:AppRemovalFailures
$targetUser = Get-TargetUserForAppRemoval
$appCount = @($appsList).Count
$appIndex = 0
$edgeIds = @('Microsoft.Edge', 'XPFFTQ037JWMHS')
$wingetRemovedApps = @()
$wingetRemovalFailures = @{}
Foreach ($app in $appsList) {
if ($script:CancelRequested) { return $false }
$appIndex++
if ($script:ApplySubStepCallback -and $appCount -gt 1) {
& $script:ApplySubStepCallback "Removing apps ($appIndex/$appCount)" $appIndex $appCount
}
Write-Host "Removing $app"
if ((Get-AppRemovalMethod $app) -eq 'WinGet') {
$removalSucceeded = Remove-WinGetApp -app $app
$wingetRemovedApps += $app
if (($script:Params.ContainsKey('User') -or $script:Params.ContainsKey('Sysprep')) -and -not $removalSucceeded) {
$wingetRemovalFailures[$app] = $true
}
}
else {
if (-not (Remove-AppxApp -app $app -targetUser $targetUser)) {
$script:AppRemovalFailures++
}
}
}
if ($script:CancelRequested) {
return $false
}
# Check whether any winget-removed apps are still present, and report errors for each one.
if ($wingetRemovedApps.Count -gt 0) {
$postRemovalList = if ($script:WingetInstalled) { Get-WingetInstalledApps -TimeOut 10 -NonBlocking } else { $null }
$edgeForceRemoveRequested = $false
$edgeForceRemoveSucceeded = $false
if ($null -eq $postRemovalList) {
$script:AppRemovalVerificationUnavailable = $true
foreach ($app in $wingetRemovedApps) {
$wingetRemovalFailures[$app] = $true
}
}
else {
foreach ($app in $wingetRemovedApps) {
if (-not (Test-AppInWingetList -appId $app -InstalledList $postRemovalList)) {
continue
}
if ($edgeIds -contains $app) {
Write-Host "Unable to uninstall Microsoft Edge via WinGet" -ForegroundColor Red
if (-not $edgeForceRemoveRequested) {
$edgeForceRemoveRequested = $true
$edgeForceRemoveSucceeded = Request-EdgeForceRemove
}
if ($edgeForceRemoveSucceeded) {
continue
}
}
else {
Write-Host "Unable to uninstall $app via WinGet" -ForegroundColor Red
}
$wingetRemovalFailures[$app] = $true
}
}
}
$script:AppRemovalFailures += $wingetRemovalFailures.Count
return ($script:AppRemovalFailures -eq $failuresBefore)
}
<#
.SYNOPSIS
Uninstalls an app via WinGet and/or schedules its removal.
.DESCRIPTION
Runs winget uninstall for a single app, with a bounded execution time.
WinGet's own exit code/success reporting is unreliable and is only logged
for diagnostics; it never causes this function to report failure. Callers
verify removal with a post-removal inventory check instead. This function
only reports failure when the winget invocation itself throws a terminating
error (e.g. it times out or cannot be started). If the User or Sysprep
parameter was passed, also schedules removal for future logins.
.PARAMETER app
The WinGet package ID to uninstall (e.g. 'Microsoft.BingNews').
.PARAMETER TimeoutSeconds
Maximum time to allow the foreground WinGet uninstall to run. Defaults
to 120 seconds.
.OUTPUTS
System.Boolean. $true unless the winget invocation threw a terminating error
or any required RunOnce scheduling failed; otherwise $false.
#>
function Remove-WinGetApp {
param(
[string]$app,
[int]$TimeoutSeconds = 120
)
if (-not $script:WingetInstalled) {
Write-Error "WinGet is either not installed or is outdated; $app could not be removed"
return $false
}
$uninstallCommandSucceeded = $true
$exitCode = $null
try {
$uninstallResult = Invoke-NonBlocking -ScriptBlock {
param($appId)
$output = @(& winget uninstall --accept-source-agreements --disable-interactivity --id $appId 2>&1)
return [PSCustomObject]@{
ExitCode = $LASTEXITCODE
Output = $output
}
} -ArgumentList $app -TimeoutSeconds $TimeoutSeconds
Write-WinGetUninstallOutput -Output $(if ($uninstallResult) { $uninstallResult.Output } else { $null })
$exitCode = if ($uninstallResult) { $uninstallResult.ExitCode } else { 'unknown' }
Write-Verbose "WinGet uninstall for $app returned exit code $exitCode."
}
catch {
$uninstallCommandSucceeded = $false
if ($_.Exception.Message -like 'Operation timed out after *') {
Write-Verbose "WinGet uninstall for $app did not complete within $TimeoutSeconds seconds: $_"
}
else {
Write-Verbose "WinGet uninstall for $app failed: $_"
}
}
$scheduleSucceeded = $true
if ($script:Params.ContainsKey("User")) {
Write-Host "Adding scheduled task to uninstall $app for user $(Get-UserName)..."
$scheduleSucceeded = Set-RunOnceWingetTask -appId $app
}
elseif ($script:Params.ContainsKey("Sysprep")) {
Write-Host "Adding scheduled task to uninstall $app for new users..."
$scheduleSucceeded = Set-RunOnceWingetTask -appId $app
}
return ($uninstallCommandSucceeded -and $scheduleSucceeded)
}
<#
.SYNOPSIS
Writes captured WinGet uninstall output to the verbose stream.
.OUTPUTS
None.
#>
function Write-WinGetUninstallOutput {
param(
[object[]]$Output
)
foreach ($line in @($Output)) {
if ($null -eq $line) { continue }
$lineText = if ($line -is [System.Management.Automation.ErrorRecord]) { $line.Exception.Message } else { $line.ToString() }
if ([string]::IsNullOrWhiteSpace($lineText)) { continue }
Write-Verbose $lineText
}
}
<#
.SYNOPSIS
Removes an app via Remove-AppxPackage / Remove-ProvisionedAppxPackage.
.PARAMETER app
The package identifier to remove (e.g. 'Clipchamp.Clipchamp').
.PARAMETER targetUser
Target scope: "AllUsers", "CurrentUser", or a specific username.
#>
function Remove-AppxApp {
param([string]$app, [string]$targetUser)
$appPattern = '*' + $app + '*'
try {
$removalResult = Invoke-NonBlocking -ScriptBlock {
param($pattern, $target)
$removalErrors = @()
$getPackageParams = @{ Name = $pattern; ErrorAction = 'Continue'; ErrorVariable = '+removalErrors' }
$removePackageParams = @{ ErrorAction = 'Continue'; ErrorVariable = '+removalErrors' }
switch ($target) {
'AllUsers' {
$getPackageParams.AllUsers = $true
$removePackageParams.AllUsers = $true
}
'CurrentUser' { }
default {
$userAccount = New-Object System.Security.Principal.NTAccount($target)
$userSid = $userAccount.Translate([System.Security.Principal.SecurityIdentifier]).Value
$getPackageParams.User = $userSid
$removePackageParams.User = $userSid
}
}
foreach ($package in @(Get-AppxPackage @getPackageParams)) {
$removePackageParams.Package = $package.PackageFullName
$null = Remove-AppxPackage @removePackageParams
}
if ($target -eq 'AllUsers') {
$provisionedPackages = @(Get-AppxProvisionedPackage -Online -ErrorAction Continue -ErrorVariable +removalErrors | Where-Object { $_.PackageName -like $pattern })
foreach ($package in $provisionedPackages) {
$null = Remove-ProvisionedAppxPackage -Online -AllUsers -PackageName $package.PackageName -ErrorAction Continue -ErrorVariable +removalErrors
}
}
return [PSCustomObject]@{ Success = ($removalErrors.Count -eq 0) }
} -ArgumentList @($appPattern, $targetUser)
}
catch {
Write-Error "Unable to remove $app via Appx: $_"
return $false
}
return [bool]($removalResult -and $removalResult.Success)
}
<#
.SYNOPSIS
Returns the removal method for an app identifier.
.DESCRIPTION
Parses Apps.json once (cached in script scope) to build a lookup of
AppId -> RemovalMethod. Returns 'WinGet' if the app should be removed
via winget, or 'Appx' if via Remove-AppxPackage. Defaults to 'Appx'
for unknown IDs.
.PARAMETER appId
The package identifier (e.g. 'Clipchamp.Clipchamp').
#>
function Get-AppRemovalMethod {
param([string]$appId)
if (-not $script:AppRemovalMethodCache) {
$script:AppRemovalMethodCache = @{}
try {
if (Test-Path $script:AppsListFilePath) {
$appsJson = Get-Content -Path $script:AppsListFilePath -Raw | ConvertFrom-Json
foreach ($appData in $appsJson.Apps) {
$rawMethod = $appData.RemovalMethod
$method = if ($rawMethod -and $rawMethod -eq 'WinGet') { 'WinGet' } else { 'Appx' }
foreach ($id in @($appData.AppId)) {
if ($id -isnot [string]) { continue }
$normalizedId = $id.Trim()
if (-not [string]::IsNullOrWhiteSpace($normalizedId)) {
$script:AppRemovalMethodCache[$normalizedId] = $method
}
}
}
}
}
catch {
Write-Warning "Failed to load app removal methods from '$script:AppsListFilePath'. Defaulting unknown apps to Appx. Error: $_"
}
}
if ($script:AppRemovalMethodCache.ContainsKey($appId)) {
return $script:AppRemovalMethodCache[$appId]
}
return 'Appx'
}
<#
.SYNOPSIS
Prompts the user to forcefully remove Microsoft Edge when winget cannot uninstall it.
.DESCRIPTION
Only invoked after it has been confirmed that Edge is still present
following all winget uninstall attempts. In GUI mode, displays a
warning message box; in CLI mode, prompts via Read-Host. On
confirmation, performs a force-remove of the Edge package.
.OUTPUTS
System.Boolean. $true when Edge is forcefully removed; otherwise $false.
#>
function Request-EdgeForceRemove {
if ($script:GuiWindow) {
$result = Show-MessageBox -Message 'Unable to uninstall Microsoft Edge via WinGet. Would you like to forcefully uninstall it? NOT RECOMMENDED!' -Title 'Force Uninstall Microsoft Edge?' -Button 'YesNo' -Icon 'Warning'
if ($result -eq 'Yes') {
Write-Host ""
return (Invoke-ForceRemoveEdge)
}
}
elseif ($(Read-Host -Prompt "Would you like to forcefully uninstall Microsoft Edge? NOT RECOMMENDED! (y/n)") -eq 'y') {
Write-Host ""
return (Invoke-ForceRemoveEdge)
}
return $false
}
<#
.SYNOPSIS
Dynamically sets a RunOnce registry key to schedule a winget uninstall.
.DESCRIPTION
Writes directly to HKEY_USERS\Default\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce
via the PowerShell registry API within Invoke-WithTargetUserHive,
which handles hive loading and HKEY_USERS\Default → SID remapping.
Used instead of static .reg files to avoid file dependency for each WinGet app.
The winget command is Base64-encoded and invoked via powershell.exe -EncodedCommand
rather than interpolated directly into cmd.exe /c. This prevents shell metacharacters
(such as &, |, <, >, ^, ") in the app ID from being interpreted as command syntax,
even if future catalog updates introduce IDs containing those characters.
.PARAMETER appId
The winget package ID to schedule for uninstall (e.g. 'XP9CXNGPPJ97XX').
#>
function Set-RunOnceWingetTask {
param([string]$appId)
$targetUserName = if ($script:Params.ContainsKey("Sysprep")) { "Default" } else { $script:Params.Item("User") }
# Sanitize appId for use in registry value names (backslashes are path separators)
$safeAppId = $appId.Replace('\', '_')
$taskName = "Uninstall_$safeAppId"
# Escape single quotes in appId, then wrap in single quotes so cmd/pwsh metacharacters
# like & | < > ^ " are treated as literals. Base64-encode the whole command so the
# RunOnce value contains only [A-Za-z0-9+/=] — safe in any shell parser.
$escapedAppId = $appId.Replace("'", "''")
$wingetCommand = "winget uninstall --accept-source-agreements --disable-interactivity --id '$escapedAppId'"
$encodedWingetCommand = [Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($wingetCommand))
$operation = [PSCustomObject]@{
KeyPath = 'HKEY_USERS\Default\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce'
ValueName = $taskName
ValueType = 'String'
ValueData = "powershell.exe -NoProfile -EncodedCommand $encodedWingetCommand"
OperationType = 'SetValue'
}
try {
Invoke-WithTargetUserHive -TargetUserName $targetUserName -ScriptBlock {
param($op)
Invoke-RegistryOperation -Operation $op -RegFilePath '<dynamic>'
} -ArgumentObject $operation
return $true
}
catch {
Write-Error "Failed to schedule uninstall task for $($appId): $_"
return $false
}
}
-132
View File
@@ -1,132 +0,0 @@
# Removes apps specified during function call based on the target scope.
function RemoveApps {
param (
$appslist
)
# Determine target from script-level params, defaulting to AllUsers
$targetUser = GetTargetUserForAppRemoval
$appIndex = 0
$appCount = @($appsList).Count
$edgeIds = @('Microsoft.Edge', 'XPFFTQ037JWMHS')
$edgeUninstallSucceeded = $false
$edgeScheduledTaskAdded = $false
Foreach ($app in $appsList) {
if ($script:CancelRequested) {
return
}
$appIndex++
# Update step name and sub-progress to show which app is being removed (only for bulk removal)
if ($script:ApplySubStepCallback -and $appCount -gt 1) {
& $script:ApplySubStepCallback "Removing apps ($appIndex/$appCount)" $appIndex $appCount
}
Write-Host "Attempting to remove $app..."
# Use WinGet only to remove OneDrive and Edge
if (($app -eq "Microsoft.OneDrive") -or ($edgeIds -contains $app)) {
if ($script:WingetInstalled -eq $false) {
Write-Host "WinGet is either not installed or is outdated, $app could not be removed" -ForegroundColor Red
continue
}
$isEdgeId = $edgeIds -contains $app
$appName = if ($isEdgeId) { 'Microsoft_Edge' } else { $app -replace '\.', '_' }
# Uninstall app via WinGet, or create a scheduled task to uninstall it later
if ($script:Params.ContainsKey("User")) {
if (-not ($isEdgeId -and $edgeScheduledTaskAdded)) {
ImportRegistryFile "Adding scheduled task to uninstall $app for user $(GetUserName)..." "Uninstall_$($appName).reg"
if ($isEdgeId) { $edgeScheduledTaskAdded = $true }
}
}
elseif ($script:Params.ContainsKey("Sysprep")) {
if (-not ($isEdgeId -and $edgeScheduledTaskAdded)) {
ImportRegistryFile "Adding scheduled task to uninstall $app after for new users..." "Uninstall_$($appName).reg"
if ($isEdgeId) { $edgeScheduledTaskAdded = $true }
}
}
else {
# Uninstall app via WinGet
$wingetOutput = Invoke-NonBlocking -ScriptBlock {
param($appId)
winget uninstall --accept-source-agreements --disable-interactivity --id $appId
} -ArgumentList $app
$wingetFailed = Select-String -InputObject $wingetOutput -Pattern "Uninstall failed with exit code|No installed package found matching input criteria|No package found matching input criteria" -SimpleMatch:$false
if ($isEdgeId) {
if (-not $wingetFailed) {
$edgeUninstallSucceeded = $true
}
# Prompt immediately after the final selected Edge ID attempt (if all attempts failed)
$hasRemainingEdgeIds = $false
if ($appIndex -lt $appCount) {
$remainingApps = @($appsList)[($appIndex)..($appCount - 1)]
$hasRemainingEdgeIds = @($remainingApps | Where-Object { $edgeIds -contains $_ }).Count -gt 0
}
if (-not $hasRemainingEdgeIds -and -not $edgeUninstallSucceeded) {
Write-Host "Unable to uninstall Microsoft Edge via WinGet" -ForegroundColor Red
if ($script:GuiWindow) {
$result = Show-MessageBox -Message 'Unable to uninstall Microsoft Edge via WinGet. Would you like to forcefully uninstall it? NOT RECOMMENDED!' -Title 'Force Uninstall Microsoft Edge?' -Button 'YesNo' -Icon 'Warning'
if ($result -eq 'Yes') {
Write-Host ""
ForceRemoveEdge
}
}
elseif ($( Read-Host -Prompt "Would you like to forcefully uninstall Microsoft Edge? NOT RECOMMENDED! (y/n)" ) -eq 'y') {
Write-Host ""
ForceRemoveEdge
}
}
}
}
continue
}
# Use Remove-AppxPackage to remove all other apps
$appPattern = '*' + $app + '*'
try {
switch ($targetUser) {
"AllUsers" {
# Remove installed app for all existing users, and from OS image
Invoke-NonBlocking -ScriptBlock {
param($pattern)
Get-AppxPackage -Name $pattern -AllUsers | Remove-AppxPackage -AllUsers -ErrorAction Continue
Get-AppxProvisionedPackage -Online | Where-Object { $_.PackageName -like $pattern } | ForEach-Object { Remove-ProvisionedAppxPackage -Online -AllUsers -PackageName $_.PackageName }
} -ArgumentList $appPattern
}
"CurrentUser" {
# Remove installed app for current user only
Invoke-NonBlocking -ScriptBlock {
param($pattern)
Get-AppxPackage -Name $pattern | Remove-AppxPackage -ErrorAction Continue
} -ArgumentList $appPattern
}
default {
# Target is a specific username - remove app for that user only
Invoke-NonBlocking -ScriptBlock {
param($pattern, $user)
$userAccount = New-Object System.Security.Principal.NTAccount($user)
$userSid = $userAccount.Translate([System.Security.Principal.SecurityIdentifier]).Value
Get-AppxPackage -Name $pattern -User $userSid | Remove-AppxPackage -User $userSid -ErrorAction Continue
} -ArgumentList @($appPattern, $targetUser)
}
}
}
catch {
Write-Verbose "Something went wrong while trying to remove $($app): $_"
}
}
Write-Host ""
}
@@ -0,0 +1,43 @@
<#
.SYNOPSIS
Checks whether an app ID appears in a parsed winget installed list.
.DESCRIPTION
Tries an exact match against the .Id property first. When that
fails, falls back to a substring search guarded by a word-boundary
regex so that short IDs don't accidentally match longer ones
(e.g. 'Microsoft.Edge' will not match 'Microsoft.EdgeDev').
.PARAMETER appId
The identifier to search for (e.g. 'Microsoft.Copilot').
.PARAMETER InstalledList
An array of PSCustomObject from Get-WingetInstalledApps.
#>
function Test-AppInWingetList {
param(
[string]$appId,
[object[]]$InstalledList
)
if (-not $InstalledList) { return $false }
# Normalize to array
$list = @($InstalledList)
# Exact match first (fast and precise)
if ($list.Id -contains $appId) {
return $true
}
# Substring fallback with word-boundary guard
$boundaryPattern = '(?<![a-zA-Z0-9])' + [regex]::Escape($appId) + '(?![a-zA-Z0-9])'
foreach ($entry in $list) {
if ($entry.Id -like "*$appId*" -and $entry.Id -match $boundaryPattern) {
return $true
}
}
return $false
}
-11
View File
@@ -1,11 +0,0 @@
function AwaitKeyToExit {
# Suppress prompt if Silent parameter was passed
if (-not $Silent) {
Write-Output ""
Write-Output "Press any key to exit..."
$null = [System.Console]::ReadKey()
}
Stop-Transcript
Exit
}
@@ -1,6 +1,6 @@
# Shows the CLI app removal menu and prompts the user to select which apps to remove.
function ShowCLIAppRemoval {
PrintHeader "App Removal"
function Show-CliAppRemoval {
Write-CliHeader "App Removal"
Write-Output "> Opening app selection form..."
@@ -8,9 +8,10 @@ function ShowCLIAppRemoval {
if ($result -eq $true) {
Write-Output "You have selected $($script:SelectedApps.Count) apps for removal"
AddParameter 'RemoveAppsCustom'
Add-Parameter 'RemoveApps'
Add-Parameter 'Apps' ($script:SelectedApps -join ',')
SaveSettings
Save-Settings
# Suppress prompt if Silent parameter was passed
if (-not $Silent) {
@@ -18,7 +19,7 @@ function ShowCLIAppRemoval {
Write-Output ""
Write-Output "Press enter to remove the selected apps or press CTRL+C to quit..."
Read-Host | Out-Null
PrintHeader "App Removal"
Write-CliHeader "App Removal"
}
}
else {
@@ -1,6 +1,6 @@
# Shows the CLI default mode app removal options. Loops until a valid option is selected.
function ShowCLIDefaultModeAppRemovalOptions {
PrintHeader 'Default Mode'
function Show-CliDefaultModeAppRemovalOptions {
Write-CliHeader 'Default Mode'
Write-Host "Please note: The default selection of apps includes Microsoft Teams, Spotify, Sticky Notes and more. Select option 2 to verify and change what apps are removed by the script" -ForegroundColor DarkGray
Write-Host ""
@@ -1,5 +1,5 @@
# Show CLI default mode options for removing apps, or set selection if RunDefaults or RunDefaultsLite parameter was passed
function ShowCLIDefaultModeOptions {
function Show-CliDefaultModeOptions {
if ($RunDefaults) {
$RemoveAppsInput = '1'
}
@@ -7,7 +7,7 @@ function ShowCLIDefaultModeOptions {
$RemoveAppsInput = '0'
}
else {
$RemoveAppsInput = ShowCLIDefaultModeAppRemovalOptions
$RemoveAppsInput = Show-CliDefaultModeAppRemovalOptions
if ($RemoveAppsInput -eq '2' -and ($script:SelectedApps.contains('Microsoft.XboxGameOverlay') -or $script:SelectedApps.contains('Microsoft.XboxGamingOverlay')) -and
$( Read-Host -Prompt "Disable Game Bar integration and game/screen recording? This also stops ms-gamingoverlay and ms-gamebar popups (y/n)" ) -eq 'y') {
@@ -15,39 +15,40 @@ function ShowCLIDefaultModeOptions {
}
}
PrintHeader 'Default Mode'
Write-CliHeader 'Default Mode'
try {
# Select app removal options based on user input
switch ($RemoveAppsInput) {
'1' {
AddParameter 'RemoveApps'
AddParameter 'Apps' 'Default'
Add-Parameter 'RemoveApps'
Add-Parameter 'Apps' 'Default'
}
'2' {
AddParameter 'RemoveAppsCustom'
Add-Parameter 'RemoveApps'
Add-Parameter 'Apps' ($script:SelectedApps -join ',')
if ($DisableGameBarIntegrationInput) {
AddParameter 'DisableDVR'
AddParameter 'DisableGameBarIntegration'
Add-Parameter 'DisableDVR'
Add-Parameter 'DisableGameBarIntegration'
}
}
}
LoadSettings -filePath $script:DefaultSettingsFilePath -expectedVersion "1.0"
Import-Settings -filePath $script:DefaultSettingsFilePath -expectedVersion "1.0"
}
catch {
Write-Error "Failed to load settings from DefaultSettings.json file: $_"
AwaitKeyToExit
Wait-ForKeyPress -ExitCode 1
}
SaveSettings
Save-Settings
if ($Silent) {
# Skip change summary and confirmation prompt
return
}
PrintPendingChanges
PrintHeader 'Default Mode'
}
Write-PendingChanges
Write-CliHeader 'Default Mode'
}
@@ -1,13 +1,13 @@
# Shows the CLI last used settings from LastUsedSettings.json file, displays pending changes and prompts the user to apply them.
function ShowCLILastUsedSettings {
PrintHeader 'Custom Mode'
function Show-CliLastUsedSettings {
Write-CliHeader 'Custom Mode'
try {
LoadSettings -filePath $script:SavedSettingsFilePath -expectedVersion "1.0"
Import-Settings -filePath $script:SavedSettingsFilePath -expectedVersion "1.0"
}
catch {
Write-Error "Failed to load settings from LastUsedSettings.json file: $_"
AwaitKeyToExit
Wait-ForKeyPress -ExitCode 1
}
if ($Silent) {
@@ -15,6 +15,6 @@ function ShowCLILastUsedSettings {
return
}
PrintPendingChanges
PrintHeader 'Custom Mode'
}
Write-PendingChanges
Write-CliHeader 'Custom Mode'
}
@@ -1,9 +1,9 @@
# Shows the CLI menu options and prompts the user to select one. Loops until a valid option is selected.
function ShowCLIMenuOptions {
function Show-CliMenuOptions {
Do {
$ModeSelectionMessage = "Please select an option (1/2)"
PrintHeader 'Menu'
Write-CliHeader 'Menu'
Write-Host "(1) Default mode: Quickly apply the recommended changes"
Write-Host "(2) App removal mode: Select & remove apps, without making other changes"
+22
View File
@@ -0,0 +1,22 @@
<#
.SYNOPSIS
Waits for user acknowledgement, then exits the script.
.PARAMETER ExitCode
Process exit code to return after acknowledgement. Defaults to 0.
#>
function Wait-ForKeyPress {
param(
[int]$ExitCode = 0
)
# Suppress prompt if Silent parameter was passed
if (-not $Silent) {
Write-Output ""
Write-Output "Press any key to exit..."
$null = [System.Console]::ReadKey()
}
Stop-Transcript
Exit $ExitCode
}
@@ -1,5 +1,5 @@
# Prints the header for the script
function PrintHeader {
function Write-CliHeader {
param (
$title
)
@@ -10,11 +10,11 @@ function PrintHeader {
$fullTitle = "$fullTitle (Sysprep mode)"
}
else {
$fullTitle = "$fullTitle (User: $(GetUserName))"
$fullTitle = "$fullTitle (User: $(Get-UserName))"
}
Clear-Host
Write-Host "-------------------------------------------------------------------------------------------"
Write-Host $fullTitle
Write-Host "-------------------------------------------------------------------------------------------"
}
}
@@ -1,5 +1,18 @@
# Prints all pending changes that will be made by the script
function PrintPendingChanges {
<#
.SYNOPSIS
Prints a summary of all pending changes to the console for the user to review.
.DESCRIPTION
Iterates over every non-control parameter in $script:Params and emits a
human-readable line for each change that will be applied. For the
'RemoveApps' parameter the list of targeted app names is displayed
inline. Feature labels are resolved from Features.json when available;
otherwise the raw parameter name is used as a fallback.
After printing the summary the function pauses until the user presses
Enter, giving them an opportunity to review and cancel via Ctrl+C.
#>
function Write-PendingChanges {
Write-Output "Win11Debloat will make the following changes:"
if ($script:Params['CreateRestorePoint']) {
@@ -19,20 +32,7 @@ function PrintPendingChanges {
continue
}
'RemoveApps' {
$appsList = GenerateAppsList
if ($appsList.Count -eq 0) {
Write-Host "No valid apps were selected for removal" -ForegroundColor Yellow
Write-Output ""
continue
}
Write-Output "- Remove $($appsList.Count) apps:"
Write-Host $appsList -ForegroundColor DarkGray
continue
}
'RemoveAppsCustom' {
$appsList = LoadAppsFromFile $script:CustomAppsListFilePath
$appsList = Generate-AppsList
if ($appsList.Count -eq 0) {
Write-Host "No valid apps were selected for removal" -ForegroundColor Yellow
@@ -45,14 +45,8 @@ function PrintPendingChanges {
continue
}
default {
if ($script:Features -and $script:Features.ContainsKey($parameterName)) {
$message = $script:Features[$parameterName].Label
Write-Output "- $message"
}
else {
# Fallback: show the parameter name if no feature description is available
Write-Output "- $parameterName"
}
$message = $script:Features[$parameterName].Label
Write-Output "- $message"
continue
}
}
@@ -62,4 +56,4 @@ function PrintPendingChanges {
Write-Output ""
Write-Output "Press enter to execute the script or press CTRL+C to quit..."
Read-Host | Out-Null
}
}
@@ -0,0 +1,14 @@
<#
.SYNOPSIS
Filters a list of features to those that have a non-empty RegistryKey.
.PARAMETER Features
An array of feature objects to filter.
#>
function Get-RegistryBackedFeatures {
param(
[object[]]$Features = @()
)
return @($Features | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_.RegistryKey) })
}
@@ -195,10 +195,19 @@ function Get-RegistryKeySnapshot {
}
}
<#
.SYNOPSIS
Converts an open registry key into a backup snapshot.
.DESCRIPTION
Captures all values or selected value names, records missing selected values,
and recursively captures subkeys when requested. Throws if a requested subkey
cannot be read.
#>
function Convert-RegistryKeyToSnapshot {
param(
[Parameter(Mandatory)]
[Microsoft.Win32.RegistryKey]$RegistryKey,
$RegistryKey,
[Parameter(Mandatory)]
[string]$FullPath,
[bool]$CaptureAllValues = $false,
@@ -233,7 +242,9 @@ function Convert-RegistryKeyToSnapshot {
if ($IncludeSubKeys) {
foreach ($subKeyName in @($RegistryKey.GetSubKeyNames())) {
$childKey = $RegistryKey.OpenSubKey($subKeyName, $false)
if ($null -eq $childKey) { continue }
if ($null -eq $childKey) {
throw "Unable to read registry subkey '$($RegistryKey.Name)\$subKeyName' while creating a backup snapshot. The backup was not created."
}
try {
$childPath = if ([string]::IsNullOrWhiteSpace($FullPath)) { $subKeyName } else { "$FullPath\$subKeyName" }
@@ -253,20 +264,34 @@ function Convert-RegistryKeyToSnapshot {
}
}
<#
.SYNOPSIS
Converts a registry value into a serializable backup snapshot.
.DESCRIPTION
Preserves the value kind and normalizes supported data types for JSON
serialization without expanding environment-string values. REG_NONE values
are rejected.
#>
function Convert-RegistryValueToSnapshot {
param(
[Parameter(Mandatory)]
[Microsoft.Win32.RegistryKey]$RegistryKey,
$RegistryKey,
[Parameter(Mandatory)]
[AllowEmptyString()]
[string]$ValueName
)
$valueKind = $RegistryKey.GetValueKind($ValueName)
if ($valueKind -eq [Microsoft.Win32.RegistryValueKind]::None) {
throw "REG_NONE registry values are not supported for backup. Key='$($RegistryKey.Name)' Name='$ValueName'"
}
$value = $RegistryKey.GetValue($ValueName, $null, [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)
try {
$normalizedValue = switch ($valueKind) {
([Microsoft.Win32.RegistryValueKind]::Binary) { @($value | ForEach-Object { [int]$_ }) }
# Prevent an empty byte sequence from being unrolled to $null by the switch pipeline.
([Microsoft.Win32.RegistryValueKind]::Binary) { if ($null -eq $value) { ,@() } else { ,@($value | ForEach-Object { [int]$_ }) } }
([Microsoft.Win32.RegistryValueKind]::MultiString) { @($value) }
([Microsoft.Win32.RegistryValueKind]::DWord) { [BitConverter]::ToUInt32([BitConverter]::GetBytes([int32]$value), 0) }
([Microsoft.Win32.RegistryValueKind]::QWord) { [BitConverter]::ToUInt64([BitConverter]::GetBytes([int64]$value), 0) }
@@ -287,12 +312,20 @@ function Convert-RegistryValueToSnapshot {
}
}
<#
.SYNOPSIS
Describes the user profile targeted by a registry backup.
.DESCRIPTION
Returns DefaultUserProfile for Sysprep, User:<name> for an explicit user,
or CurrentUser:<name> otherwise.
#>
function Get-RegistryBackupTargetDescription {
if ($script:Params.ContainsKey('Sysprep')) {
return 'DefaultUserProfile'
}
$resolvedUserName = [string](GetUserName)
$resolvedUserName = [string](Get-UserName)
if ($script:Params.ContainsKey('User')) {
return "User:$resolvedUserName"
@@ -1,3 +1,18 @@
<#
.SYNOPSIS
Creates a timestamped JSON backup of registry state for selected features.
.DESCRIPTION
Resolves selected and undo features from the provided keys, captures their
registry state, and saves the result as a JSON file in the Backups/ folder.
Returns the file path on success, $null if no registry-backed features exist.
.PARAMETER ActionableKeys
Param keys from $script:Params to resolve into apply features.
.PARAMETER ExtraFeatures
Additional synthetic feature objects (e.g. undo features) to include.
#>
function New-RegistrySettingsBackup {
param(
[string[]]$ActionableKeys,
@@ -22,7 +37,7 @@ function New-RegistrySettingsBackup {
$backupFilePath = Join-Path $backupDirectory $backupFileName
$backupConfig = Get-RegistryBackupPayload -SelectedFeatures $selectedFeatures -UndoFeatures $undoFeatures -CreatedAt $timestamp
if (-not (SaveToFile -Config $backupConfig -FilePath $backupFilePath -MaxDepth 25)) {
if (-not (Save-ToFile -Config $backupConfig -FilePath $backupFilePath -MaxDepth 25)) {
throw "Failed to save registry backup to '$backupFilePath'"
}
@@ -32,6 +47,13 @@ function New-RegistrySettingsBackup {
return $backupFilePath
}
<#
.SYNOPSIS
Resolves param keys into deduplicated feature objects from the catalog.
.PARAMETER ActionableKeys
Param keys to look up in $script:Features.
#>
function Get-SelectedFeatures {
param(
[string[]]$ActionableKeys
@@ -46,8 +68,7 @@ function Get-SelectedFeatures {
$feature = $script:Features[$paramKey]
if (-not $feature) { continue }
$featureId = Get-FeatureId -Feature $feature
$featureId = [string]$feature.FeatureId
if ($selectedFeatureIds.Add($featureId)) {
$selectedFeatures.Add($feature)
}
@@ -56,6 +77,23 @@ function Get-SelectedFeatures {
return @($selectedFeatures.ToArray())
}
<#
.SYNOPSIS
Builds the full backup payload object from selected and undo features.
.DESCRIPTION
Deduplicates feature IDs, resolves registry capture plans, snapshots all
registry keys, and assembles the final backup hashtable with metadata.
.PARAMETER SelectedFeatures
Feature objects from the apply side.
.PARAMETER UndoFeatures
Synthetic feature objects from the undo side.
.PARAMETER CreatedAt
Timestamp recorded in the backup metadata.
#>
function Get-RegistryBackupPayload {
param(
[object[]]$SelectedFeatures = @(),
@@ -67,8 +105,7 @@ function Get-RegistryBackupPayload {
$selectedFeatureIds = New-Object System.Collections.Generic.List[string]
$seenSelectedFeatureIds = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
foreach ($feature in $SelectedFeatures) {
$featureId = Get-FeatureId -Feature $feature
$featureId = [string]$feature.FeatureId
if ($seenSelectedFeatureIds.Add($featureId)) {
$selectedFeatureIds.Add($featureId)
}
@@ -77,8 +114,7 @@ function Get-RegistryBackupPayload {
$selectedUndoFeatureIds = New-Object System.Collections.Generic.List[string]
$seenUndoFeatureIds = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
foreach ($feature in $UndoFeatures) {
$featureId = Get-FeatureId -Feature $feature
$featureId = [string]$feature.FeatureId
if ($seenUndoFeatureIds.Add($featureId)) {
$selectedUndoFeatureIds.Add($featureId)
}
@@ -1,21 +0,0 @@
function Get-FeatureId {
param(
[Parameter(Mandatory)]
$Feature
)
$featureId = [string]$Feature.FeatureId
if ([string]::IsNullOrWhiteSpace($featureId)) {
throw 'Selected feature is missing required FeatureId.'
}
return $featureId
}
function Get-RegistryBackedFeatures {
param(
[object[]]$Features = @()
)
return @($Features | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_.RegistryKey) })
}
@@ -1,185 +0,0 @@
# Disables Microsoft Store search suggestions in the start menu for all users by denying access to the Store app database file for each user
function DisableStoreSearchSuggestionsForAllUsers {
# Get path to Store app database for all users
$userPathString = GetUserDirectory -userName "*" -fileName "AppData\Local\Packages"
$usersStoreDbPaths = Get-ChildItem -Path $userPathString -ErrorAction SilentlyContinue
# Go through all users and disable start search suggestions
ForEach ($storeDbPath in $usersStoreDbPaths) {
DisableStoreSearchSuggestions ($storeDbPath.FullName + "\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db")
}
# Also disable start search suggestions for the default user profile
$defaultStoreDbPath = GetUserDirectory -userName "Default" -fileName "AppData\Local\Packages\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db" -exitIfPathNotFound $false
DisableStoreSearchSuggestions $defaultStoreDbPath
}
# Disables Microsoft Store search suggestions in the start menu by denying access to the Store app database file
function DisableStoreSearchSuggestions {
param (
$StoreAppsDatabase = "$env:LocalAppData\Packages\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db"
)
# Change path to correct user if a user was specified
if ($script:Params.ContainsKey("User")) {
$StoreAppsDatabase = GetUserDirectory -userName "$(GetUserName)" -fileName "AppData\Local\Packages\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db" -exitIfPathNotFound $false
}
$userName = [regex]::Match($StoreAppsDatabase, '(?:Users\\)([^\\]+)(?:\\AppData)').Groups[1].Value
# This file doesn't exist in EEA (No Store app suggestions).
if (-not (Test-Path -Path $StoreAppsDatabase))
{
Write-Host "Unable to find Store app database for user $userName, creating it now to prevent Windows from creating it later..." -ForegroundColor Yellow
$storeDbDir = Split-Path -Path $StoreAppsDatabase -Parent
if (-not (Test-Path -Path $storeDbDir)) {
New-Item -Path $storeDbDir -ItemType Directory -Force | Out-Null
}
New-Item -Path $StoreAppsDatabase -ItemType File -Force | Out-Null
}
$AccountSid = [System.Security.Principal.SecurityIdentifier]::new('S-1-1-0') # 'EVERYONE' group
$Acl = Get-Acl -Path $StoreAppsDatabase
$Ace = [System.Security.AccessControl.FileSystemAccessRule]::new($AccountSid, 'FullControl', 'Deny')
$Acl.SetAccessRule($Ace) | Out-Null
Set-Acl -Path $StoreAppsDatabase -AclObject $Acl | Out-Null
Write-Host "Disabled Microsoft Store search suggestions for user $userName"
}
function EnableStoreSearchSuggestionsForAllUsers {
# Get path to Store app database for all users
$userPathString = GetUserDirectory -userName "*" -fileName "AppData\Local\Packages"
$usersStoreDbPaths = Get-ChildItem -Path $userPathString -ErrorAction SilentlyContinue
# Go through all users and re-enable start search suggestions
ForEach ($storeDbPath in $usersStoreDbPaths) {
EnableStoreSearchSuggestions ($storeDbPath.FullName + "\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db")
}
# Also re-enable for the default user profile
$defaultStoreDbPath = GetUserDirectory -userName "Default" -fileName "AppData\Local\Packages\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db" -exitIfPathNotFound $false
EnableStoreSearchSuggestions $defaultStoreDbPath
}
function EnableStoreSearchSuggestions {
param (
$StoreAppsDatabase = "$env:LocalAppData\Packages\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db"
)
# Change path to correct user if a user was specified
if ($script:Params.ContainsKey("User")) {
$StoreAppsDatabase = GetUserDirectory -userName "$(GetUserName)" -fileName "AppData\Local\Packages\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db" -exitIfPathNotFound $false
}
$userName = [regex]::Match($StoreAppsDatabase, '(?:Users\\)([^\\]+)(?:\\AppData)').Groups[1].Value
if (-not $userName) { $userName = '<unknown>' }
if (-not (Test-Path -Path $StoreAppsDatabase)) {
Write-Host "Store app database not found for user $userName, nothing to undo"
return
}
# Ensure we can modify/delete the file even if restrictive ACLs were set.
$global:LASTEXITCODE = 0
takeown /F "$StoreAppsDatabase" /A | Out-Null
icacls "$StoreAppsDatabase" /grant *S-1-5-32-544:F /C | Out-Null
$everyoneSid = [System.Security.Principal.SecurityIdentifier]::new('S-1-1-0') # 'EVERYONE' group
try {
$acl = Get-Acl -Path $StoreAppsDatabase
$denyRules = @(
$acl.Access | Where-Object {
$_.AccessControlType -eq [System.Security.AccessControl.AccessControlType]::Deny -and
(($_.FileSystemRights -band [System.Security.AccessControl.FileSystemRights]::FullControl) -ne 0) -and
(try { $_.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]) -eq $everyoneSid } catch { $false })
}
)
foreach ($denyRule in $denyRules) {
$null = $acl.RemoveAccessRuleSpecific($denyRule)
}
Set-Acl -Path $StoreAppsDatabase -AclObject $acl | Out-Null
}
catch {
Write-Warning "Failed to normalize ACL for store database '$StoreAppsDatabase': $($_.Exception.Message)"
}
try {
Remove-Item -Path $StoreAppsDatabase -Force -ErrorAction Stop
Write-Host "Re-enabled Microsoft Store search suggestions for user $userName"
}
catch {
throw "Failed to remove '$StoreAppsDatabase' while undoing Microsoft Store search suggestions for user $userName. $($_.Exception.Message)"
}
}
function Test-StoreSearchSuggestionsDisabled {
param(
[Parameter(Mandatory)]
[string]$StoreAppsDatabase
)
if (-not (Test-Path -Path $StoreAppsDatabase)) {
return $false
}
try {
$acl = Get-Acl -Path $StoreAppsDatabase
}
catch {
return $false
}
$everyoneSid = [System.Security.Principal.SecurityIdentifier]::new('S-1-1-0')
foreach ($accessRule in @($acl.Access)) {
$isDenyFullControl = $accessRule.AccessControlType -eq [System.Security.AccessControl.AccessControlType]::Deny -and
(($accessRule.FileSystemRights -band [System.Security.AccessControl.FileSystemRights]::FullControl) -ne 0)
if (-not $isDenyFullControl) { continue }
$isEveryone = $false
try {
$isEveryone = $accessRule.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]) -eq $everyoneSid
} catch { }
if ($isEveryone) {
return $true
}
}
return $false
}
function Test-StoreSearchSuggestionsDisabledForAllUsers {
$paths = @()
$userPathString = GetUserDirectory -userName "*" -fileName "AppData\Local\Packages"
$usersStoreDbPaths = Get-ChildItem -Path $userPathString -ErrorAction SilentlyContinue
foreach ($storeDbPath in $usersStoreDbPaths) {
$paths += ($storeDbPath.FullName + "\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db")
}
$defaultStoreDbPath = GetUserDirectory -userName "Default" -fileName "AppData\Local\Packages\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db" -exitIfPathNotFound $false
if ($defaultStoreDbPath) {
$paths += $defaultStoreDbPath
}
if ($paths.Count -eq 0) {
return $false
}
foreach ($path in $paths) {
if (-not (Test-StoreSearchSuggestionsDisabled -StoreAppsDatabase $path)) {
return $false
}
}
return $true
}
-309
View File
@@ -1,309 +0,0 @@
# Executes a single parameter/feature based on its key
# Parameters:
# $paramKey - The parameter name to execute
function ExecuteParameter {
param (
[string]$paramKey
)
# Check if this feature has metadata in Features.json
$feature = $null
if ($script:Features.ContainsKey($paramKey)) {
$feature = $script:Features[$paramKey]
}
# If feature has RegistryKey and ApplyText, use dynamic ImportRegistryFile
if ($feature -and $feature.RegistryKey -and $feature.ApplyText) {
ImportRegistryFile "> $($feature.ApplyText)" $feature.RegistryKey
# Handle special cases that have additional logic after ImportRegistryFile
switch ($paramKey) {
'DisableBing' {
# Also remove the app package for Bing search
RemoveApps @('Microsoft.BingSearch')
}
'DisableCopilot' {
# Also remove the app package for Copilot
RemoveApps @('Microsoft.Copilot')
}
}
return
}
# Handle features without RegistryKey or with special logic
switch ($paramKey) {
'RemoveApps' {
Write-Host "> $($feature.ApplyText) for $(GetFriendlyTargetUserName)..."
$appsList = GenerateAppsList
if ($appsList.Count -eq 0) {
Write-Host "No valid apps were selected for removal" -ForegroundColor Yellow
Write-Host ""
return
}
Write-Host "$($appsList.Count) apps selected for removal"
RemoveApps $appsList
}
'RemoveAppsCustom' {
Write-Host "> $($feature.ApplyText)..."
$appsList = LoadAppsFromFile $script:CustomAppsListFilePath
if ($appsList.Count -eq 0) {
Write-Host "No valid apps were selected for removal" -ForegroundColor Yellow
Write-Host ""
return
}
Write-Host "$($appsList.Count) apps selected for removal"
RemoveApps $appsList
}
'RemoveGamingApps' {
$appsList = @('Microsoft.GamingApp', 'Microsoft.XboxGameOverlay', 'Microsoft.XboxGamingOverlay')
Write-Host "> $($feature.ApplyText)..."
RemoveApps $appsList
return
}
'RemoveHPApps' {
$appsList = @('AD2F1837.HPAIExperienceCenter', 'AD2F1837.HPJumpStarts', 'AD2F1837.HPPCHardwareDiagnosticsWindows', 'AD2F1837.HPPowerManager', 'AD2F1837.HPPrivacySettings', 'AD2F1837.HPSupportAssistant', 'AD2F1837.HPSureShieldAI', 'AD2F1837.HPSystemInformation', 'AD2F1837.HPQuickDrop', 'AD2F1837.HPWorkWell', 'AD2F1837.myHP', 'AD2F1837.HPDesktopSupportUtilities', 'AD2F1837.HPQuickTouch', 'AD2F1837.HPEasyClean', 'AD2F1837.HPConnectedMusic', 'AD2F1837.HPFileViewer', 'AD2F1837.HPRegistration', 'AD2F1837.HPWelcome', 'AD2F1837.HPConnectedPhotopoweredbySnapfish', 'AD2F1837.HPPrinterControl')
Write-Host "> $($feature.ApplyText)..."
RemoveApps $appsList
return
}
'DisableWidgets' {
Write-Host "> $($feature.ApplyText)..."
# Stop widgets related processes before removing the app packages to prevent potential issues
Get-Process *Widget* -ErrorAction SilentlyContinue | Stop-Process
RemoveApps @('Microsoft.StartExperiencesApp','MicrosoftWindows.Client.WebExperience','Microsoft.WidgetsPlatformRuntime')
}
'EnableWindowsSandbox' {
Write-Host "> $($feature.ApplyText)..."
EnableWindowsFeature "Containers-DisposableClientVM"
Write-Host ""
return
}
'EnableWindowsSubsystemForLinux' {
Write-Host "> $($feature.ApplyText)..."
EnableWindowsFeature "VirtualMachinePlatform"
EnableWindowsFeature "Microsoft-Windows-Subsystem-Linux"
Write-Host ""
return
}
'ClearStart' {
Write-Host "> $($feature.ApplyText) for user $(GetUserName)..."
ReplaceStartMenu
Write-Host ""
return
}
'ReplaceStart' {
Write-Host "> $($feature.ApplyText) for user $(GetUserName)..."
ReplaceStartMenu $script:Params.Item("ReplaceStart")
Write-Host ""
return
}
'ClearStartAllUsers' {
ReplaceStartMenuForAllUsers
return
}
'ReplaceStartAllUsers' {
ReplaceStartMenuForAllUsers $script:Params.Item("ReplaceStartAllUsers")
return
}
'DisableStoreSearchSuggestions' {
if ($script:Params.ContainsKey("Sysprep")) {
Write-Host "> Disabling Microsoft Store search suggestions in the start menu for all users..."
DisableStoreSearchSuggestionsForAllUsers
Write-Host ""
return
}
Write-Host "> Disabling Microsoft Store search suggestions for user $(GetUserName)..."
DisableStoreSearchSuggestions
Write-Host ""
return
}
}
}
# Executes all selected parameters/features
function ExecuteAllChanges {
# When running as SYSTEM, require -User or -Sysprep to prevent applying
# changes to the SYSTEM profile instead of a real user.
$isSystem = ([Security.Principal.WindowsIdentity]::GetCurrent().User.Value -eq 'S-1-5-18')
if ($isSystem -and -not $script:Params.ContainsKey("User") -and -not $script:Params.ContainsKey("Sysprep")) {
throw "Win11Debloat is running as the SYSTEM account. Use the '-User' or '-Sysprep' parameter to target a specific user."
}
$script:RegistryImportFailures = 0
# Build list of actionable parameters (skip control params and data-only params)
$actionableKeys = @()
foreach ($paramKey in $script:Params.Keys) {
if ($script:ControlParams -contains $paramKey) { continue }
if ($paramKey -eq 'Apps') { continue }
if ($paramKey -eq 'CreateRestorePoint') { continue }
$actionableKeys += $paramKey
}
$hasRegistryBackedFeature = $false
foreach ($paramKey in $actionableKeys) {
if (-not $script:Features.ContainsKey($paramKey)) { continue }
$feature = $script:Features[$paramKey]
if ($feature -and -not [string]::IsNullOrWhiteSpace([string]$feature.RegistryKey)) {
$hasRegistryBackedFeature = $true
break
}
}
# Undo operations that write registry values also require a backup
if (-not $hasRegistryBackedFeature) {
foreach ($featureId in $script:UndoParams.Keys) {
$f = if ($script:Features.ContainsKey($featureId)) { $script:Features[$featureId] } else { $null }
if ($f -and $f.RegistryUndoKey) { $hasRegistryBackedFeature = $true; break }
}
}
$totalSteps = $actionableKeys.Count + $script:UndoParams.Count
if ($hasRegistryBackedFeature) { $totalSteps++ }
if ($script:Params.ContainsKey("CreateRestorePoint")) { $totalSteps++ }
$currentStep = 0
if ($hasRegistryBackedFeature) {
$currentStep++
if ($script:ApplyProgressCallback) {
& $script:ApplyProgressCallback $currentStep $totalSteps "Creating registry backup..."
}
Write-Host "> Creating registry backup..."
try {
$undoSyntheticFeatures = @($script:UndoParams.Keys | ForEach-Object {
$f = if ($script:Features.ContainsKey($_)) { $script:Features[$_] } else { $null }
if ($f -and $f.RegistryUndoKey) {
[PSCustomObject]@{ FeatureId = $_; RegistryKey = (Resolve-UndoRegFilePath $f.RegistryUndoKey) }
}
} | Where-Object { $_ })
New-RegistrySettingsBackup -ActionableKeys $actionableKeys -ExtraFeatures $undoSyntheticFeatures | Out-Null
}
catch {
throw "Registry backup failed before applying changes. $($_.Exception.Message)"
}
}
# Create restore point if requested (CLI only - GUI handles this separately)
if ($script:Params.ContainsKey("CreateRestorePoint")) {
$currentStep++
if ($script:ApplyProgressCallback) {
& $script:ApplyProgressCallback $currentStep $totalSteps "Creating system restore point, this may take a moment..."
}
Write-Host "> Creating a system restore point..."
CreateSystemRestorePoint
Write-Host ""
}
# Execute all parameters
foreach ($paramKey in $actionableKeys) {
if ($script:CancelRequested) { return }
$currentStep++
# Get friendly name for the step
$stepName = $paramKey
if ($script:Features.ContainsKey($paramKey)) {
$feature = $script:Features[$paramKey]
if ($feature.ApplyText) {
# Prefer explicit ApplyText when provided
$stepName = $feature.ApplyText
} elseif ($feature.Label) {
# Fallback: use label from Features.json
$stepName = $feature.Label
}
}
if ($script:ApplyProgressCallback) {
& $script:ApplyProgressCallback $currentStep $totalSteps $stepName
}
ExecuteParameter -paramKey $paramKey
}
# Execute all undo operations
foreach ($featureId in $script:UndoParams.Keys) {
if ($script:CancelRequested) { return }
$f = if ($script:Features.ContainsKey($featureId)) { $script:Features[$featureId] } else { $null }
$undoLabel = if ($f -and $f.UndoLabel) { $f.UndoLabel } else { $featureId }
$applyUndoText = if ($f -and $f.ApplyUndoText) { $f.ApplyUndoText } else { $undoLabel }
$currentStep++
if ($script:ApplyProgressCallback) {
& $script:ApplyProgressCallback $currentStep $totalSteps $applyUndoText
}
if ($f -and $f.RegistryUndoKey) {
ImportRegistryFile "> $applyUndoText" (Resolve-UndoRegFilePath $f.RegistryUndoKey)
} else {
Invoke-UndoFeatureAction -FeatureId $featureId
}
}
if ($script:RegistryImportFailures -gt 0) {
Write-Host ""
Write-Host "$($script:RegistryImportFailures) registry import change(s) failed. See output above for details." -ForegroundColor Yellow
}
}
# Resolves the path of an undo reg file relative to $script:RegfilesPath.
# Checks the Undo/ subfolder first, then falls back to the root Regfiles/ folder.
function Resolve-UndoRegFilePath {
param ([string]$FileName)
$undoSubPath = Join-Path 'Undo' $FileName
if (Test-Path (Join-Path $script:RegfilesPath $undoSubPath)) {
return $undoSubPath
}
return $FileName
}
function Invoke-UndoFeatureAction {
param(
[Parameter(Mandatory)]
[string]$FeatureId
)
$feature = if ($script:Features.ContainsKey($FeatureId)) { $script:Features[$FeatureId] } else { $null }
switch ($FeatureId) {
'DisableStoreSearchSuggestions' {
if ($script:Params.ContainsKey('Sysprep')) {
Write-Host "> Re-enabling Microsoft Store search suggestions in the start menu for all users..."
EnableStoreSearchSuggestionsForAllUsers
Write-Host ""
return
}
Write-Host "> Re-enabling Microsoft Store search suggestions for user $(GetUserName)..."
EnableStoreSearchSuggestions
Write-Host ""
return
}
'EnableWindowsSandbox' {
Write-Host "> $($feature.ApplyUndoText)..."
DisableWindowsFeature 'Containers-DisposableClientVM'
Write-Host ""
return
}
'EnableWindowsSubsystemForLinux' {
Write-Host "> $($feature.ApplyUndoText)..."
DisableWindowsFeature 'Microsoft-Windows-Subsystem-Linux'
DisableWindowsFeature 'VirtualMachinePlatform'
Write-Host ""
return
}
default {
Write-Host "> No undo action defined for $FeatureId, skipping..." -ForegroundColor Yellow
Write-Host ""
return
}
}
}
@@ -1,6 +1,10 @@
# Tests whether the registry operations in a feature's .reg file currently match the live registry.
# Returns $true if ALL operations in the apply reg file match current system state.
# Returns $false if the feature has no RegistryKey, the file is missing, or any operation mismatches.
<#
.SYNOPSIS
Maps a .reg file value type string to its RegistryValueKind enum.
.PARAMETER Operation
A parsed .reg operation object containing a ValueType property.
#>
function Get-ExpectedRegistryValueKind {
param(
[Parameter(Mandatory)]
@@ -18,13 +22,25 @@ function Get-ExpectedRegistryValueKind {
}
}
<#
.SYNOPSIS
Tests whether a feature's registry operations currently match the live registry.
.DESCRIPTION
Returns $true when ALL operations in the apply .reg file match current system
state. Returns $false if the feature has no RegistryKey, the reg file is
missing, or any operation mismatches. Special-cased features (Widgets, Store
suggestions, Windows Sandbox, WSL) bypass .reg checking entirely.
.PARAMETER FeatureId
The feature identifier to test.
#>
function Test-FeatureApplied {
param (
[Parameter(Mandatory)]
[string]$FeatureId
)
if (-not $script:Features.ContainsKey($FeatureId)) { return $false }
$feature = $script:Features[$FeatureId]
switch ($FeatureId) {
@@ -50,10 +66,7 @@ function Test-FeatureApplied {
return (Test-StoreSearchSuggestionsDisabledForAllUsers)
}
$storeDbPath = "$env:LocalAppData\Packages\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db"
if ($script:Params.ContainsKey('User')) {
$storeDbPath = GetUserDirectory -userName "$(GetUserName)" -fileName "AppData\Local\Packages\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db" -exitIfPathNotFound $false
}
$storeDbPath = Get-StoreAppsDatabasePathForUser -UserName (Get-UserName)
return (Test-StoreSearchSuggestionsDisabled -StoreAppsDatabase $storeDbPath)
}
@@ -150,8 +163,18 @@ function Test-FeatureApplied {
return $true
}
# Returns the 1-based index of the UiGroup option whose features all match current system state,
# or 0 if no option fully matches (meaning the current state is unknown / "No Change").
<#
.SYNOPSIS
Returns the 1-based index of the UiGroup option whose features all match
current system state.
.DESCRIPTION
Returns 0 if no option fully matches, meaning the current state is unknown
or represents "No Change".
.PARAMETER Group
A UiGroup object whose Values array contains options with FeatureIds.
#>
function Get-CurrentGroupActiveIndex {
param (
[Parameter(Mandatory)]
+120
View File
@@ -0,0 +1,120 @@
<#
.SYNOPSIS
Imports and executes a registry file.
.OUTPUTS
System.Boolean. $true when the registry file is applied or previewed successfully; otherwise $false.
#>
function Import-RegistryFile {
param (
$message,
$path
)
try {
Write-Host $message
$usesOfflineHive = $script:Params.ContainsKey("Sysprep") -or $script:Params.ContainsKey("User")
$regFilePath = Get-RegistryFilePathForFeature -RegistryKey $path
if (-not (Test-Path $regFilePath)) {
$errorMessage = "Unable to find registry file: $path ($regFilePath)"
Write-Host "Error: $errorMessage" -ForegroundColor Red
return $false
}
$importScript = {
param($targetRegFilePath, $hiveContext)
if ($script:Params.ContainsKey("WhatIf")) {
return (Invoke-RegistryOperationsFromRegFile -RegFilePath $targetRegFilePath)
}
# When the target user's hive is already loaded under their SID, the .reg file's
# HKEY_USERS\Default paths won't match. Use the PowerShell registry writer instead,
# which remaps Default → SID via Split-RegistryPath.
$usePowerShellFallbackOnly = $hiveContext -and [bool]$hiveContext.WasAlreadyLoaded
if ($usePowerShellFallbackOnly) {
$fallbackSucceeded = Invoke-RegistryOperationsFromRegFile -RegFilePath $targetRegFilePath
if ($fallbackSucceeded) {
Write-Host "The operation completed successfully via PowerShell registry writer."
}
return $fallbackSucceeded
}
$regResult = Invoke-NonBlocking -ScriptBlock {
param($targetRegFilePath)
$result = @{
Output = @()
ExitCode = 0
Error = $null
}
try {
$global:LASTEXITCODE = 0
$output = reg import $targetRegFilePath 2>&1
$importExitCode = $LASTEXITCODE
if ($output) {
$result.Output = @($output)
}
$result.ExitCode = $importExitCode
if ($importExitCode -ne 0) {
throw "Registry import failed with exit code $importExitCode for '$targetRegFilePath'"
}
}
catch {
$result.Error = $_.Exception.Message
$result.ExitCode = if ($LASTEXITCODE -ne 0) { $LASTEXITCODE } else { 1 }
}
return $result
} -ArgumentList $targetRegFilePath
$regOutput = @($regResult.Output)
$hasSuccess = ($regResult.ExitCode -eq 0) -and -not $regResult.Error
if ($regOutput) {
foreach ($line in $regOutput) {
$lineText = if ($line -is [System.Management.Automation.ErrorRecord]) { $line.Exception.Message } else { $line.ToString() }
if ($lineText -and $lineText.Length -gt 0) {
if ($hasSuccess) {
Write-Host $lineText
}
else {
Write-Host $lineText -ForegroundColor Red
}
}
}
}
if (-not $hasSuccess) {
$details = if ($regResult.Error) { $regResult.Error } else { "Exit code: $($regResult.ExitCode)" }
Write-Warning "reg import failed for '$path'. Falling back to PowerShell registry writer. Details: $details"
$fallbackSucceeded = Invoke-RegistryOperationsFromRegFile -RegFilePath $targetRegFilePath
if ($fallbackSucceeded) {
Write-Host "The operation completed successfully via PowerShell registry writer."
}
return $fallbackSucceeded
}
return $true
}
if ($usesOfflineHive) {
# Sysprep targets Default user, User targets the specified user. Logged-in users already have their hive mounted under HKU\<SID>.
$targetUserName = if ($script:Params.ContainsKey("Sysprep")) { "Default" } else { $script:Params.Item("User") }
$succeeded = Invoke-WithTargetUserHive -TargetUserName $targetUserName -ScriptBlock $importScript -ArgumentObject $regFilePath -PassHiveContext
}
else {
$succeeded = & $importScript $regFilePath $null
}
return [bool]$succeeded
}
catch {
Write-Host $_.Exception.Message -ForegroundColor Red
return $false
}
}
-108
View File
@@ -1,108 +0,0 @@
# Import & execute regfile
function ImportRegistryFile {
param (
$message,
$path
)
Write-Host $message
$usesOfflineHive = $script:Params.ContainsKey("Sysprep") -or $script:Params.ContainsKey("User")
$regFilePath = Get-RegistryFilePathForFeature -RegistryKey $path
if (-not (Test-Path $regFilePath)) {
$errorMessage = "Unable to find registry file: $path ($regFilePath)"
$script:RegistryImportFailures++
Write-Host "Error: $errorMessage" -ForegroundColor Red
Write-Host ""
throw $errorMessage
}
$importScript = {
param($targetRegFilePath, $hiveContext)
# When the target user's hive is already loaded under their SID, the .reg file's
# HKEY_USERS\Default paths won't match. Use the PowerShell registry writer instead,
# which remaps Default → SID via Split-RegistryPath.
$usePowerShellFallbackOnly = $hiveContext -and [bool]$hiveContext.WasAlreadyLoaded
if ($usePowerShellFallbackOnly) {
Invoke-RegistryOperationsFromRegFile -RegFilePath $targetRegFilePath
Write-Host "The operation completed successfully via PowerShell registry writer."
Write-Host ""
return
}
$regResult = Invoke-NonBlocking -ScriptBlock {
param($targetRegFilePath)
$result = @{
Output = @()
ExitCode = 0
Error = $null
}
try {
$global:LASTEXITCODE = 0
$output = reg import $targetRegFilePath 2>&1
$importExitCode = $LASTEXITCODE
if ($output) {
$result.Output = @($output)
}
$result.ExitCode = $importExitCode
if ($importExitCode -ne 0) {
throw "Registry import failed with exit code $importExitCode for '$targetRegFilePath'"
}
}
catch {
$result.Error = $_.Exception.Message
$result.ExitCode = if ($LASTEXITCODE -ne 0) { $LASTEXITCODE } else { 1 }
}
return $result
} -ArgumentList $targetRegFilePath
$regOutput = @($regResult.Output)
$hasSuccess = ($regResult.ExitCode -eq 0) -and -not $regResult.Error
if ($regOutput) {
foreach ($line in $regOutput) {
$lineText = if ($line -is [System.Management.Automation.ErrorRecord]) { $line.Exception.Message } else { $line.ToString() }
if ($lineText -and $lineText.Length -gt 0) {
if ($hasSuccess) {
Write-Host $lineText
}
else {
Write-Host $lineText -ForegroundColor Red
}
}
}
}
if (-not $hasSuccess) {
$details = if ($regResult.Error) { $regResult.Error } else { "Exit code: $($regResult.ExitCode)" }
Write-Warning "reg import failed for '$path'. Falling back to PowerShell registry writer. Details: $details"
Invoke-RegistryOperationsFromRegFile -RegFilePath $targetRegFilePath
Write-Host "The operation completed successfully via PowerShell registry writer."
}
Write-Host ""
}
try {
if ($usesOfflineHive) {
# Sysprep targets Default user, User targets the specified user. Logged-in users already have their hive mounted under HKU\<SID>.
$targetUserName = if ($script:Params.ContainsKey("Sysprep")) { "Default" } else { $script:Params.Item("User") }
Invoke-WithTargetUserHive -TargetUserName $targetUserName -ScriptBlock $importScript -ArgumentObject $regFilePath -PassHiveContext
}
else {
& $importScript $regFilePath $null
}
}
catch {
$script:RegistryImportFailures++
Write-Host $_.Exception.Message -ForegroundColor Red
Write-Host ""
}
}
+493
View File
@@ -0,0 +1,493 @@
<#
.SYNOPSIS
Applies a single feature/debloat operation.
.DESCRIPTION
Handles two categories of features:
- Registry-backed: imports the .reg file via Import-RegistryFile, then runs
any post-import side effects (e.g., removing companion app packages).
- Custom logic: app removal, Windows optional features, start menu
replacement, and other special-case features. Returns $true when the
feature completes successfully; otherwise writes a warning and returns
$false.
#>
function Invoke-FeatureApply {
param(
[Parameter(Mandatory)]
[string]$FeatureId
)
try {
# Resolve feature metadata from Features.json
$feature = $script:Features[$FeatureId]
$applyText = $feature.ApplyText
# ---- Registry-backed features: import .reg file, then handle additional tasks ----
if ($feature.RegistryKey) {
if (-not (Import-RegistryFile "> $applyText..." $feature.RegistryKey)) {
return $false
}
switch ($FeatureId) {
'DisableBing' {
# Also remove the app package for Bing search
return (Remove-SelectedApps @('Microsoft.BingSearch'))
}
'DisableCopilot' {
# Also remove the app packages for Copilot
return (Remove-SelectedApps @('Microsoft.Copilot', 'XP9CXNGPPJ97XX'))
}
'DisableTelemetry' {
# Also disable telemetry scheduled tasks
return (Disable-TelemetryScheduledTasks)
}
}
return $true
}
# ---- Custom features (no registry backing, or special handling required) ----
switch ($FeatureId) {
'RemoveApps' {
Write-Host "> $applyText for $(Get-FriendlyTargetUserName)..."
$appsList = Generate-AppsList
if ($appsList.Count -eq 0) {
Write-Host "No valid apps were selected for removal" -ForegroundColor Yellow
return $true
}
Write-Host "$($appsList.Count) apps selected for removal"
return (Remove-SelectedApps $appsList)
}
'RemoveGamingApps' {
$appsList = @('Microsoft.GamingApp', 'Microsoft.XboxGameOverlay', 'Microsoft.XboxGamingOverlay')
Write-Host "> $applyText..."
return (Remove-SelectedApps $appsList)
}
'RemoveHPApps' {
$appsList = @('AD2F1837.HPAIExperienceCenter', 'AD2F1837.HPJumpStarts', 'AD2F1837.HPPCHardwareDiagnosticsWindows', 'AD2F1837.HPPowerManager', 'AD2F1837.HPPrivacySettings', 'AD2F1837.HPSupportAssistant', 'AD2F1837.HPSureShieldAI', 'AD2F1837.HPSystemInformation', 'AD2F1837.HPQuickDrop', 'AD2F1837.HPWorkWell', 'AD2F1837.myHP', 'AD2F1837.HPDesktopSupportUtilities', 'AD2F1837.HPQuickTouch', 'AD2F1837.HPEasyClean', 'AD2F1837.HPConnectedMusic', 'AD2F1837.HPFileViewer', 'AD2F1837.HPRegistration', 'AD2F1837.HPWelcome', 'AD2F1837.HPConnectedPhotopoweredbySnapfish', 'AD2F1837.HPPrinterControl')
Write-Host "> $applyText..."
return (Remove-SelectedApps $appsList)
}
'ForceRemoveEdge' {
Write-Host "> $applyText..."
return (Invoke-ForceRemoveEdge)
}
'DisableWidgets' {
Write-Host "> $applyText..."
# Stop widgets related processes before removing the app packages to prevent potential issues
if (-not $script:Params.ContainsKey("WhatIf")) {
Get-Process *Widget* -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
}
return (Remove-SelectedApps @('Microsoft.StartExperiencesApp','MicrosoftWindows.Client.WebExperience','Microsoft.WidgetsPlatformRuntime'))
}
'EnableWindowsSandbox' {
Write-Host "> $applyText..."
return (Enable-WindowsFeature "Containers-DisposableClientVM")
}
'EnableWindowsSubsystemForLinux' {
Write-Host "> $applyText..."
if (-not (Enable-WindowsFeature "VirtualMachinePlatform")) { return $false }
return (Enable-WindowsFeature "Microsoft-Windows-Subsystem-Linux")
}
'ClearStart' {
Write-Host "> $applyText for user $(Get-UserName)..."
$startMenuBinFile = Get-StartMenuBinPathForUser -UserName (Get-UserName)
if (-not [string]::IsNullOrWhiteSpace($startMenuBinFile)) {
return (Replace-StartMenu -startMenuBinFile $startMenuBinFile)
}
Write-Warning "Unable to apply '$applyText': the Start menu path for user $(Get-UserName) could not be resolved."
return $false
}
'ReplaceStart' {
Write-Host "> $applyText for user $(Get-UserName)..."
$startMenuBinFile = Get-StartMenuBinPathForUser -UserName (Get-UserName)
if (-not [string]::IsNullOrWhiteSpace($startMenuBinFile)) {
return (Replace-StartMenu -startMenuBinFile $startMenuBinFile -startMenuTemplate $script:Params.Item("ReplaceStart"))
}
Write-Warning "Unable to apply '$applyText': the Start menu path for user $(Get-UserName) could not be resolved."
return $false
}
'ClearStartAllUsers' {
return (Replace-StartMenuForAllUsers)
}
'ReplaceStartAllUsers' {
return (Replace-StartMenuForAllUsers -startMenuTemplate $script:Params.Item("ReplaceStartAllUsers"))
}
'DisableStoreSearchSuggestions' {
if ($script:Params.ContainsKey("Sysprep")) {
Write-Host "> Disabling Microsoft Store search suggestions in the start menu for all users..."
return (Set-StoreSearchSuggestionsDisabledForAllUsers)
}
Write-Host "> Disabling Microsoft Store search suggestions for user $(Get-UserName)..."
$storeDb = Get-StoreAppsDatabasePathForUser -UserName (Get-UserName)
if ($storeDb) {
return (Set-StoreSearchSuggestionsDisabled -StoreAppsDatabase $storeDb)
}
Write-Warning "Unable to disable Microsoft Store search suggestions because the Store database for user $(Get-UserName) could not be resolved."
return $false
}
}
}
catch {
Write-Warning "Failed to apply '$applyText': $($_.Exception.Message)"
return $false
}
Write-Warning "Unknown feature '$FeatureId' could not be applied."
return $false
}
<#
.SYNOPSIS
Undoes a single feature.
.DESCRIPTION
Handles registry-backed undo imports and custom undo logic. Returns
$true when the requested undo succeeds; otherwise writes a warning and
returns $false.
#>
function Invoke-FeatureUndo {
param(
[Parameter(Mandatory)]
[string]$FeatureId
)
$feature = if ($script:Features.ContainsKey($FeatureId)) { $script:Features[$FeatureId] } else { $null }
if (-not $feature) {
Write-Warning "Unknown feature '$FeatureId' could not be undone."
return $false
}
$undoText = if ($feature.ApplyUndoText) { $feature.ApplyUndoText } elseif ($feature.UndoLabel) { $feature.UndoLabel } else { $FeatureId }
try {
# ---- Registry-backed features: import undo data, then handle additional tasks ----
if ($feature.RegistryUndoKey) {
if (-not (Import-RegistryFile "> $undoText" (Resolve-UndoRegFilePath $feature.RegistryUndoKey))) {
return $false
}
switch ($FeatureId) {
'DisableTelemetry' {
# Also re-enable telemetry scheduled tasks.
return (Enable-TelemetryScheduledTasks)
}
}
return $true
}
# ---- Custom undo features (no registry backing) ----
switch ($FeatureId) {
'DisableStoreSearchSuggestions' {
if ($script:Params.ContainsKey('Sysprep')) {
Write-Host "> Re-enabling Microsoft Store search suggestions in the start menu for all users..."
return (Set-StoreSearchSuggestionsEnabledForAllUsers)
}
Write-Host "> Re-enabling Microsoft Store search suggestions for user $(Get-UserName)..."
$storeDb = Get-StoreAppsDatabasePathForUser -UserName (Get-UserName)
if ($storeDb) {
return (Set-StoreSearchSuggestionsEnabled -StoreAppsDatabase $storeDb)
}
Write-Warning "Unable to re-enable Microsoft Store search suggestions because the Store database for user $(Get-UserName) could not be resolved."
return $false
}
'EnableWindowsSandbox' {
Write-Host "> $undoText..."
return (Disable-WindowsFeature 'Containers-DisposableClientVM')
}
'EnableWindowsSubsystemForLinux' {
Write-Host "> $undoText..."
if (-not (Disable-WindowsFeature 'Microsoft-Windows-Subsystem-Linux')) { return $false }
return (Disable-WindowsFeature 'VirtualMachinePlatform')
}
}
}
catch {
Write-Warning "Failed to undo '$undoText': $($_.Exception.Message)"
return $false
}
Write-Warning "Feature '$FeatureId' does not support undo."
return $false
}
<#
.SYNOPSIS
Resolves the path of an undo .reg file relative to $script:RegfilesPath.
.DESCRIPTION
Checks the Undo/ subfolder first, then falls back to the root Regfiles/
folder. This allows undo files to be organized separately from apply files.
#>
function Resolve-UndoRegFilePath {
param([string]$FileName)
$undoSubPath = Join-Path 'Undo' $FileName
if (Test-Path (Join-Path $script:RegfilesPath $undoSubPath)) {
return $undoSubPath
}
return $FileName
}
<#
.SYNOPSIS
Applies a list of features, reporting progress for each.
.DESCRIPTION
Iterates through the provided feature IDs and calls Invoke-FeatureApply
for each. Handles progress callbacks (GUI mode) and cancellation checks.
This is called by Invoke-AllChanges during the apply phase.
#>
function Invoke-ApplyFeatures {
param(
[Parameter(Mandatory)]
[string[]]$FeatureIds,
[Parameter(Mandatory)]
[int]$StartStep,
[Parameter(Mandatory)]
[int]$TotalSteps
)
if ($FeatureIds.Count -eq 0) { return }
$step = $StartStep
foreach ($featureId in $FeatureIds) {
if ($script:CancelRequested) { return }
# Resolve display name for the progress indicator
$f = $script:Features[$featureId]
$displayName = $f.ApplyText
if ($script:ApplyProgressCallback) {
& $script:ApplyProgressCallback $step $TotalSteps $displayName
}
# Compare app-removal failure counts so a feature that only fails due to
# app removal isn't also double-reported as a feature failure.
$appRemovalFailuresBefore = $script:AppRemovalFailures
if ((-not (Invoke-FeatureApply -FeatureId $featureId)) -and ($script:AppRemovalFailures -eq $appRemovalFailuresBefore)) {
$script:FeatureFailures++
}
Write-Host ""
$step++
}
}
<#
.SYNOPSIS
Undoes a list of features, reporting progress for each.
.DESCRIPTION
Iterates through the provided feature IDs and delegates each to
Invoke-FeatureUndo, which handles registry-backed and custom undo logic.
This is called by Invoke-AllChanges during the undo phase.
#>
function Invoke-UndoFeatures {
param(
[Parameter(Mandatory)]
[string[]]$FeatureIds,
[Parameter(Mandatory)]
[int]$StartStep,
[Parameter(Mandatory)]
[int]$TotalSteps
)
if ($FeatureIds.Count -eq 0) { return }
$step = $StartStep
foreach ($featureId in $FeatureIds) {
if ($script:CancelRequested) { return }
$f = if ($script:Features.ContainsKey($featureId)) { $script:Features[$featureId] } else { $null }
$undoLabel = if ($f -and $f.UndoLabel) { $f.UndoLabel } else { $featureId }
$undoText = if ($f -and $f.ApplyUndoText) { $f.ApplyUndoText } else { $undoLabel }
if ($script:ApplyProgressCallback) {
& $script:ApplyProgressCallback $step $TotalSteps $undoText
}
if (-not (Invoke-FeatureUndo -FeatureId $featureId)) {
$script:FeatureFailures++
}
Write-Host ""
$step++
}
}
<#
.SYNOPSIS
Main orchestrator: applies and undoes all selected features.
.DESCRIPTION
Sequenced in four phases:
1. Registry backup (skipped when SkipRegistryBackup is present)
2. System restore point (skipped when CreateRestorePoint is absent)
3. Apply phase - applies all selected features via Invoke-ApplyFeatures
4. Undo phase - undoes selected features via Invoke-UndoFeatures
Progress is reported through $script:ApplyProgressCallback when set
(used by the GUI modal). Cancellation is checked between each step.
#>
function Invoke-AllChanges {
if ($script:CancelRequested) { return }
# Guard: prevent running as SYSTEM account without explicit target user
$isSystem = Test-RunningAsSystem
if ($isSystem -and -not $script:Params.ContainsKey("User") -and -not $script:Params.ContainsKey("Sysprep")) {
throw "Win11Debloat is running as the SYSTEM account. Use the '-User' or '-Sysprep' parameter to target a specific user."
}
$script:AppRemovalFailures = 0
$script:FeatureFailures = 0
$script:AppRemovalVerificationUnavailable = $false
# ---- Gather work items ----
$applyIds = @()
foreach ($key in $script:Params.Keys) {
if ($script:ControlParams -contains $key) { continue }
if ($key -eq 'Apps') { continue }
if ($key -eq 'CreateRestorePoint') { continue }
$applyIds += $key
}
$undoIds = @($script:UndoParams.Keys)
# ---- Determine if registry backup is needed ----
$needsBackup = $false
foreach ($id in $applyIds) {
$f = $script:Features[$id]
if ($f -and -not [string]::IsNullOrWhiteSpace([string]$f.RegistryKey)) {
$needsBackup = $true
break
}
}
if (-not $needsBackup) {
foreach ($id in $undoIds) {
$f = if ($script:Features.ContainsKey($id)) { $script:Features[$id] } else { $null }
if ($f -and $f.RegistryUndoKey) { $needsBackup = $true; break }
}
}
# ---- Calculate total progress steps ----
$totalSteps = $applyIds.Count + $undoIds.Count
if ($needsBackup -and -not $script:Params.ContainsKey('SkipRegistryBackup')) { $totalSteps++ }
if ($script:Params.ContainsKey("CreateRestorePoint")) { $totalSteps++ }
$step = 0
# ================================================================
# Phase 1: Registry backup
# ================================================================
if ($needsBackup -and -not $script:Params.ContainsKey('SkipRegistryBackup')) {
if ($script:CancelRequested) { return }
$step++
if ($script:ApplyProgressCallback) {
& $script:ApplyProgressCallback $step $totalSteps "Creating registry backup..."
}
if ($script:Params.ContainsKey("WhatIf")) {
Write-Host "[WhatIf] Create registry backup" -ForegroundColor Cyan
}
else {
Write-Host "> Creating registry backup..."
try {
$undoSyntheticFeatures = @($undoIds | ForEach-Object {
$f = if ($script:Features.ContainsKey($_)) { $script:Features[$_] } else { $null }
if ($f -and $f.RegistryUndoKey) {
[PSCustomObject]@{ FeatureId = $_; RegistryKey = (Resolve-UndoRegFilePath $f.RegistryUndoKey) }
}
} | Where-Object { $_ })
New-RegistrySettingsBackup -ActionableKeys $applyIds -ExtraFeatures $undoSyntheticFeatures | Out-Null
}
catch {
throw "Registry backup failed before applying changes. $($_.Exception.Message)"
}
}
}
# ================================================================
# Phase 2: System restore point
# ================================================================
if ($script:Params.ContainsKey("CreateRestorePoint")) {
if ($script:CancelRequested) { return }
$step++
if ($script:ApplyProgressCallback) {
& $script:ApplyProgressCallback $step $totalSteps "Creating system restore point, this may take a moment..."
}
if ($script:Params.ContainsKey("WhatIf")) {
Write-Host "[WhatIf] Create system restore point" -ForegroundColor Cyan
Write-Host ""
}
else {
Write-Host "> Creating a system restore point..."
$restorePointSucceeded = Invoke-SystemRestorePoint
if (-not $restorePointSucceeded) {
if ($script:CancelRequested) { return }
$script:FeatureFailures++
}
Write-Host ""
}
}
# ================================================================
# Phase 3: Apply features
# ================================================================
if ($applyIds.Count -gt 0) {
Invoke-ApplyFeatures -FeatureIds $applyIds -StartStep ($step + 1) -TotalSteps $totalSteps
$step += $applyIds.Count
}
if ($script:CancelRequested) { return }
# ================================================================
# Phase 4: Undo features
# ================================================================
if ($undoIds.Count -gt 0) {
Invoke-UndoFeatures -FeatureIds $undoIds -StartStep ($step + 1) -TotalSteps $totalSteps
$step += $undoIds.Count
}
# ================================================================
# Final: Report failures
# ================================================================
if ($script:AppRemovalFailures -gt 0) {
Write-Host ""
Write-Warning "$($script:AppRemovalFailures) app removal(s) failed. See output above for details."
}
if ($script:FeatureFailures -gt 0) {
Write-Host ""
Write-Warning "$($script:FeatureFailures) feature change(s) failed. See output above for details."
}
if ($script:AppRemovalVerificationUnavailable) {
Write-Host ""
Write-Warning "Unable to verify if all apps were uninstalled successfully."
}
}
<#
.SYNOPSIS
Tests whether Win11Debloat is running under the SYSTEM account.
.DESCRIPTION
Compares the current Windows identity's security identifier (SID) with
the well-known Local System SID (S-1-5-18).
.OUTPUTS
System.Boolean
Returns $true when the current process runs as SYSTEM; otherwise, $false.
#>
function Test-RunningAsSystem {
return ([Security.Principal.WindowsIdentity]::GetCurrent().User.Value -eq 'S-1-5-18')
}
@@ -1,25 +1,29 @@
# 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")) {
<#
.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 Invoke-RestartExplorer {
if ($script:Params.ContainsKey("WhatIf")) {
Write-Host "[WhatIf] Restart the Windows Explorer process" -ForegroundColor Cyan
return
}
Write-Host "> Attempting to restart the Windows Explorer process to apply all changes..."
if ($script:Params.ContainsKey("NoRestartExplorer")) {
if ($script:Params.ContainsKey('SkipExplorerRestart')) {
Write-Host "Explorer process restart was skipped, please manually reboot your PC to apply all changes" -ForegroundColor Yellow
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.
# Only restart if the PowerShell process matches the OS architecture.
# Restarting explorer from a 32bit PowerShell window will fail on a 64bit OS
if ([Environment]::Is64BitProcess -eq [Environment]::Is64BitOperatingSystem) {
Write-Host "Restarting the Windows Explorer process... (This may cause your screen to flicker)"
@@ -28,4 +32,4 @@ function RestartExplorer {
else {
Write-Host "Unable to restart Windows Explorer process, please manually reboot your PC to apply all changes" -ForegroundColor Yellow
}
}
}
@@ -1,10 +1,25 @@
function CreateSystemRestorePoint {
$SysRestore = Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\SystemRestore" -Name "RPSessionInterval"
$failed = $false
<#
.SYNOPSIS
Creates a system restore point.
if ($SysRestore.RPSessionInterval -eq 0) {
.OUTPUTS
System.Boolean. $true when a restore point is created; otherwise $false.
#>
function Invoke-SystemRestorePoint {
$failed = $false
$isSilent = ($script:Params -and $script:Params.ContainsKey('Silent')) -or $script:Silent
try {
$SysRestore = Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\SystemRestore" -Name "RPSessionInterval" -ErrorAction Stop
}
catch {
Write-Host "Error: Unable to determine whether System Restore is enabled: $($_.Exception.Message)" -ForegroundColor Red
$failed = $true
}
if (-not $failed -and $SysRestore.RPSessionInterval -eq 0) {
# In GUI mode, skip the prompt and just try to enable it
if ($script:GuiWindow -or $Silent -or $( Read-Host -Prompt "System restore is disabled, would you like to enable it and create a restore point? (y/n)") -eq 'y') {
if ($script:GuiWindow -or $isSilent -or $( Read-Host -Prompt "System restore is disabled, would you like to enable it and create a restore point? (y/n)") -eq 'y') {
try {
$enableResult = Invoke-NonBlocking -TimeoutSeconds 90 -ScriptBlock {
try {
@@ -26,7 +41,6 @@ function CreateSystemRestorePoint {
}
}
else {
Write-Host ""
$failed = $true
}
}
@@ -79,17 +93,20 @@ function CreateSystemRestorePoint {
if ($result -ne "Yes") {
$script:CancelRequested = $true
return
return $false
}
}
elseif (-not $Silent) {
elseif (-not $isSilent) {
Write-Host "Failed to create a system restore point. Do you want to continue without a restore point? (y/n)" -ForegroundColor Yellow
if ($( Read-Host ) -ne 'y') {
$script:CancelRequested = $true
return
return $false
}
}
Write-Host "Warning: Continuing without restore point" -ForegroundColor Yellow
return $false
}
return $true
}
@@ -248,6 +248,14 @@ function New-RegistryBackupAllowListPlanMap {
return $planMap
}
<#
.SYNOPSIS
Converts registry value names into a case-insensitive set.
.DESCRIPTION
Preserves empty names and prevents PowerShell from enumerating the returned
HashSet.
#>
function ConvertTo-RegistryValueNameSet {
param(
[AllowEmptyCollection()]
@@ -259,9 +267,18 @@ function ConvertTo-RegistryValueNameSet {
$null = $valueNameSet.Add([string]$valueName)
}
return $valueNameSet
# Prevent PowerShell from enumerating the HashSet into an array or single string
return ,$valueNameSet
}
<#
.SYNOPSIS
Validates a registry snapshot against the selected-feature allow list.
.DESCRIPTION
Recursively validates snapshot paths, value names, value kinds, and value
data, appending validation errors to the supplied list.
#>
function Test-RegistrySnapshotAgainstAllowList {
param(
[Parameter(Mandatory)]
@@ -300,6 +317,9 @@ function Test-RegistrySnapshotAgainstAllowList {
if (-not (Test-RegistryValueKindNameSupported -KindName $kindName)) {
$Errors.Add("Backup contains unsupported registry value kind '$kindName' for '$valueReference'.")
}
elseif (-not (Test-RegistryValueDataMatchesKind -KindName $kindName -Data $valueSnapshot.Data)) {
$Errors.Add("Backup contains invalid registry data for kind '$kindName' at '$valueReference'.")
}
}
elseif (-not [string]::IsNullOrWhiteSpace($kindName)) {
$Errors.Add("Backup value '$valueReference' must not define Kind when Exists is false.")
@@ -311,6 +331,64 @@ function Test-RegistrySnapshotAgainstAllowList {
}
}
<#
.SYNOPSIS
Tests whether backed-up registry data is valid for its declared value kind.
.DESCRIPTION
Rejects corrupted or hand-edited backup data that cannot be restored safely,
such as a DWord that overflows UInt32 or binary data containing an invalid byte.
This validation runs before Restore-RegistryKeySnapshot mutates the live
registry, preventing a failed conversion from leaving a partially restored key.
.PARAMETER KindName
The declared registry value kind name, such as DWord, QWord, or Binary.
.PARAMETER Data
The backed-up value data to validate against the declared kind.
.OUTPUTS
System.Boolean
#>
function Test-RegistryValueDataMatchesKind {
param(
[Parameter(Mandatory)]
[string]$KindName,
[AllowNull()]
$Data
)
$kind = [System.Enum]::Parse([Microsoft.Win32.RegistryValueKind], $KindName, $true)
switch ($kind) {
([Microsoft.Win32.RegistryValueKind]::DWord) {
$parsed = [uint32]0
return [uint32]::TryParse([string]$Data, [System.Globalization.NumberStyles]::Integer, [System.Globalization.CultureInfo]::InvariantCulture, [ref]$parsed)
}
([Microsoft.Win32.RegistryValueKind]::QWord) {
$parsed = [uint64]0
return [uint64]::TryParse([string]$Data, [System.Globalization.NumberStyles]::Integer, [System.Globalization.CultureInfo]::InvariantCulture, [ref]$parsed)
}
([Microsoft.Win32.RegistryValueKind]::Binary) {
if ($null -eq $Data -or $Data -isnot [array]) { return $false }
foreach ($item in @($Data)) {
if ($item -isnot [ValueType] -and $item -isnot [string]) { return $false }
$parsed = 0
if (-not [int]::TryParse([string]$item, [ref]$parsed) -or $parsed -lt 0 -or $parsed -gt 255) {
return $false
}
}
return $true
}
([Microsoft.Win32.RegistryValueKind]::MultiString) {
foreach ($item in @($Data)) {
if ($item -isnot [string]) { return $false }
}
return $true
}
default { return ($null -eq $Data -or $Data -is [string]) }
}
}
function Test-RegistryValueAllowedByPlan {
param(
[Parameter(Mandatory)]
@@ -385,7 +463,7 @@ function Find-RegistryAllowListPlanMatch {
continue
}
$subKeyPrefix = "$($plan.NormalizedPath)\\"
$subKeyPrefix = "$($plan.NormalizedPath)\"
if ($NormalizedPath.StartsWith($subKeyPrefix, [System.StringComparison]::OrdinalIgnoreCase)) {
return [PSCustomObject]@{
IsDescendant = $true
@@ -428,6 +506,14 @@ function Get-NormalizedRegistryPathKey {
return "$normalizedHive\\$normalizedSubKey"
}
<#
.SYNOPSIS
Tests whether a registry value-kind name is supported in backups.
.DESCRIPTION
Parses kind names case-insensitively and rejects empty, invalid, Unknown,
and None values.
#>
function Test-RegistryValueKindNameSupported {
param(
[string]$KindName
@@ -439,9 +525,10 @@ function Test-RegistryValueKindNameSupported {
try {
$kind = [System.Enum]::Parse([Microsoft.Win32.RegistryValueKind], $KindName, $true)
return $kind -ne [Microsoft.Win32.RegistryValueKind]::Unknown
return $kind -notin @([Microsoft.Win32.RegistryValueKind]::Unknown, [Microsoft.Win32.RegistryValueKind]::None)
}
catch {
return $false
}
}
+475
View File
@@ -0,0 +1,475 @@
<#
.SYNOPSIS
Replaces the start menu layout for all user profiles.
.DESCRIPTION
Iterates over every existing user profile and the Default user profile,
replacing each user's start2.bin file with the specified template. When
using the default template, this clears all pinned apps from the start menu.
Credit: https://lazyadmin.nl/win-11/customize-windows-11-start-menu-layout/
.PARAMETER startMenuTemplate
Path to the .bin template file to apply. Defaults to the blank template
bundled with the script (Assets/Start/start2.bin).
.EXAMPLE
Replace-StartMenuForAllUsers
.EXAMPLE
Replace-StartMenuForAllUsers -startMenuTemplate "C:\CustomLayout.bin"
.OUTPUTS
System.Boolean. $true when all resolved profiles are updated or the change is previewed; otherwise $false.
#>
function Replace-StartMenuForAllUsers {
param (
[string]$startMenuTemplate = "$script:AssetsPath\Start\start2.bin"
)
Write-Host "> Removing all pinned apps from the start menu for all users..."
# Check if template bin file exists
if (-not (Test-Path $startMenuTemplate)) {
Write-Host "Error: Unable to clear start menu, start2.bin file missing from script folder" -ForegroundColor Red
return $false
}
# Get path to start menu file for all users
$userPathString = Get-UserDirectory -userName "*" -fileName "AppData\Local\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState"
$usersStartMenuPaths = Get-ChildItem -Path $userPathString -ErrorAction SilentlyContinue
# Go through all users and replace the start menu file
$success = $true
ForEach ($startMenuPath in $usersStartMenuPaths) {
if (-not (Replace-StartMenu -startMenuBinFile "$($startMenuPath.Fullname)\start2.bin" -startMenuTemplate $startMenuTemplate)) {
$success = $false
}
}
# Also replace the start menu file for the default user profile
$defaultStartMenuPath = Get-UserDirectory -userName "Default" -fileName "AppData\Local\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState" -exitIfPathNotFound $false
if ($script:Params.ContainsKey("WhatIf")) {
Write-Host "[WhatIf] Replace Start Menu for Default user profile with template $startMenuTemplate" -ForegroundColor Cyan
return $true
}
# Create folder if it doesn't exist
if (-not (Test-Path $defaultStartMenuPath)) {
try {
New-Item $defaultStartMenuPath -ItemType Directory -Force -ErrorAction Stop | Out-Null
Write-Host "Created LocalState folder for default user profile"
}
catch {
Write-Warning "Failed to create the Default profile Start Menu directory: $($_.Exception.Message)"
return $false
}
}
# Copy template to default profile
if (-not (Replace-StartMenu -startMenuBinFile "$($defaultStartMenuPath)\start2.bin" -startMenuTemplate $startMenuTemplate)) {
$success = $false
}
else {
Write-Host "Replaced start menu for the default user profile"
}
return $success
}
<#
.SYNOPSIS
Replaces the start menu layout for a single user.
.DESCRIPTION
Backs up the current start2.bin file (if it exists), then copies the
specified template over it. When using the default template this clears
all pinned apps from the start menu. Validates that the template file
exists and has a .bin extension before proceeding.
Credit: https://lazyadmin.nl/win-11/customize-windows-11-start-menu-layout/
.PARAMETER startMenuBinFile
The full path to the user's start2.bin file to replace.
.PARAMETER startMenuTemplate
Path to the .bin template file to apply. Defaults to the blank template
bundled with the script (Assets/Start/start2.bin).
.EXAMPLE
Replace-StartMenu -startMenuBinFile "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin"
.EXAMPLE
Replace-StartMenu -startMenuBinFile "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin" -startMenuTemplate "C:\CustomLayout.bin"
.OUTPUTS
System.Boolean. $true when the template is valid and copied, or the change is previewed; otherwise $false.
#>
function Replace-StartMenu {
param (
[Parameter(Mandatory)]
[string]$startMenuBinFile,
[string]$startMenuTemplate = "$script:AssetsPath\Start\start2.bin"
)
# Check if template bin file exists
if (-not (Test-Path $startMenuTemplate)) {
Write-Host "Error: Unable to replace start menu, template file not found" -ForegroundColor Red
return $false
}
if ([IO.Path]::GetExtension($startMenuTemplate) -ne ".bin") {
Write-Host "Error: Unable to replace start menu, template file is not a valid .bin file" -ForegroundColor Red
return $false
}
$userName = Get-StartMenuUserNameFromPath -StartMenuBinFile $startMenuBinFile
if ($script:Params.ContainsKey("WhatIf")) {
Write-Host "[WhatIf] Replace Start Menu for user $userName with template $startMenuTemplate" -ForegroundColor Cyan
return $true
}
$timestamp = Get-Date -Format 'yyyyMMdd_HHmmss'
$backupFileName = "Win11Debloat-StartBackup-$timestamp.bak"
$startMenuDir = Split-Path $startMenuBinFile -Parent
$backupBinFile = Join-Path $startMenuDir $backupFileName
try {
if (Test-Path $startMenuBinFile) {
# Backup current start menu file
Copy-Item -Path $startMenuBinFile -Destination $backupBinFile -Force -ErrorAction Stop
Write-Verbose "Start menu backup for user $userName saved to $backupFileName"
}
else {
Write-Host "Unable to find original start2.bin file for user $userName, no backup was created for this user" -ForegroundColor Yellow
New-Item -ItemType File -Path $startMenuBinFile -Force -ErrorAction Stop | Out-Null
}
# Copy template file
Copy-Item -Path $startMenuTemplate -Destination $startMenuBinFile -Force -ErrorAction Stop
}
catch {
Write-Warning "Failed to replace Start Menu for user ${userName}: $($_.Exception.Message)"
return $false
}
Write-Host "Replaced start menu for user $userName"
return $true
}
<#
.SYNOPSIS
Returns the full path to the start menu bin file for a given user.
.DESCRIPTION
Resolves the path to the start2.bin file for the specified username.
When no username is provided or the value is empty, falls back to
the current user's local app data path via $env:LOCALAPPDATA.
.PARAMETER UserName
The target username. Pass an empty string or omit to resolve for the current user.
.EXAMPLE
Get-StartMenuBinPathForUser -UserName "Jeff"
.EXAMPLE
Get-StartMenuBinPathForUser -UserName "Default"
#>
function Get-StartMenuBinPathForUser {
param(
[string]$UserName
)
if ([string]::IsNullOrWhiteSpace($UserName)) {
return "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin"
}
return (Get-UserDirectory -userName $UserName -fileName "AppData\Local\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin" -exitIfPathNotFound $false)
}
<#
.SYNOPSIS
Extracts the username from a start2.bin file path.
.DESCRIPTION
Parses a typical C:\Users\<UserName>\AppData\... path and returns the
username portion. Returns 'unknown' if the path does not match the
expected pattern.
.PARAMETER StartMenuBinFile
The full path to a start2.bin file.
.EXAMPLE
Get-StartMenuUserNameFromPath -StartMenuBinFile "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin"
#>
function Get-StartMenuUserNameFromPath {
param(
[string]$StartMenuBinFile
)
$resolvedUserName = [regex]::Match($StartMenuBinFile, '(?:Users\\)([^\\]+)(?:\\AppData)').Groups[1].Value
if ([string]::IsNullOrWhiteSpace($resolvedUserName)) {
return 'unknown'
}
return $resolvedUserName
}
<#
.SYNOPSIS
Returns the path to the latest start menu backup file for the given scope.
.DESCRIPTION
Resolves the LocalState folder for the specified scope and returns the
full path to the most recent Win11Debloat-StartBackup-*.bak file, or
$null if no backup exists.
For CurrentUser, uses $env:LOCALAPPDATA directly. For AllUsers, scans
every user profile.
.PARAMETER Scope
The scope to check: CurrentUser or AllUsers.
.EXAMPLE
$backupPath = Get-StartMenuBackupPath -Scope 'CurrentUser'
.EXAMPLE
$backupPath = Get-StartMenuBackupPath -Scope 'AllUsers'
#>
function Get-StartMenuBackupPath {
param(
[Parameter(Mandatory)]
[ValidateSet('CurrentUser', 'AllUsers')]
[string]$Scope
)
if ($Scope -eq 'CurrentUser') {
$localStateDir = "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState"
$latestBackup = Get-ChildItem -Path (Join-Path $localStateDir 'Win11Debloat-StartBackup-*.bak') -ErrorAction SilentlyContinue |
Sort-Object Name -Descending |
Select-Object -First 1
if ($latestBackup) { return $latestBackup.FullName }
return $null
}
else {
$userPathString = Get-UserDirectory -userName "*" -fileName "AppData\Local\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState"
$usersStartMenuPaths = Get-ChildItem -Path $userPathString -ErrorAction SilentlyContinue
foreach ($startMenuPath in $usersStartMenuPaths) {
$latestBackup = Get-ChildItem -Path (Join-Path $startMenuPath.FullName 'Win11Debloat-StartBackup-*.bak') -ErrorAction SilentlyContinue |
Sort-Object Name -Descending |
Select-Object -First 1
if ($latestBackup) { return $latestBackup.FullName }
}
return $null
}
}
<#
.SYNOPSIS
Restores a user's start menu from a backup file.
.DESCRIPTION
Moves the current start2.bin to a .restore.bak safety copy, then copies
the specified backup file into place. Returns a PSCustomObject with
UserName, Result ($true/$false), and Message properties describing
the outcome.
.PARAMETER StartMenuBinFile
The full path to the user's start2.bin file to restore.
.PARAMETER BackupFilePath
Path to the backup file to restore from. If omitted, automatically
finds the latest Win11Debloat-StartBackup-*.bak file.
.EXAMPLE
Restore-StartMenuFromBackup -StartMenuBinFile "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin"
.EXAMPLE
Restore-StartMenuFromBackup -StartMenuBinFile "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin" -BackupFilePath "C:\Backups\Win11Debloat-StartBackup-20260101_120000.bak"
#>
function Restore-StartMenuFromBackup {
param(
[Parameter(Mandatory)]
[string]$StartMenuBinFile,
[string]$BackupFilePath
)
$userName = Get-StartMenuUserNameFromPath -StartMenuBinFile $StartMenuBinFile
$backupBinFile = if ([string]::IsNullOrWhiteSpace($BackupFilePath)) {
# Auto-detect latest backup in the same folder as the start2.bin
$startMenuDir = Split-Path $StartMenuBinFile -Parent
$latestBackup = Get-ChildItem -Path (Join-Path $startMenuDir 'Win11Debloat-StartBackup-*.bak') -ErrorAction SilentlyContinue |
Sort-Object Name -Descending |
Select-Object -First 1
if ($latestBackup) { $latestBackup.FullName } else { $null }
}
else {
$BackupFilePath
}
$restoreTimestamp = Get-Date -Format 'yyyyMMdd_HHmmss'
$restoreBackupFileName = "Win11Debloat-StartRestore-$restoreTimestamp.bak"
$currentBinBackup = Join-Path (Split-Path $StartMenuBinFile -Parent) $restoreBackupFileName
if ([string]::IsNullOrWhiteSpace($backupBinFile)) {
return [PSCustomObject]@{
UserName = $userName
Result = $false
Message = "No start menu backup file found for user $userName."
}
}
if ($script:Params.ContainsKey("WhatIf")) {
Write-Host "[WhatIf] Restore start menu for user $userName from backup $backupBinFile" -ForegroundColor Cyan
return [PSCustomObject]@{
UserName = $userName
Result = $true
Message = "[WhatIf] Restored start menu for user $userName."
}
}
if (-not (Test-Path -LiteralPath $backupBinFile)) {
return [PSCustomObject]@{
UserName = $userName
Result = $false
Message = "No start menu backup file found for user $userName."
}
}
try {
if (Test-Path -LiteralPath $StartMenuBinFile) {
Move-Item -Path $StartMenuBinFile -Destination $currentBinBackup -Force
}
Copy-Item -Path $backupBinFile -Destination $StartMenuBinFile -Force
return [PSCustomObject]@{
UserName = $userName
Result = $true
Message = "Restored start menu for user $userName."
}
}
catch {
return [PSCustomObject]@{
UserName = $userName
Result = $false
Message = "Failed to restore start menu for user $userName. $($_.Exception.Message)"
}
}
}
<#
.SYNOPSIS
Restores the start menu for the current target user from a backup.
.DESCRIPTION
Resolves the start2.bin path for the currently logged-in user, then
delegates to Restore-StartMenuFromBackup.
.PARAMETER BackupFilePath
Path to the backup file to restore from. If omitted, automatically
finds the latest Win11Debloat-StartBackup-*.bak file.
.EXAMPLE
Restore-StartMenu
.EXAMPLE
Restore-StartMenu -BackupFilePath "C:\Backups\Win11Debloat-StartBackup-20260101_120000.bak"
#>
function Restore-StartMenu {
param(
[string]$BackupFilePath
)
$targetUserName = $env:USERNAME
$startMenuBinFile = "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin"
Write-Host "Restoring start menu for user $targetUserName from backup..."
return Restore-StartMenuFromBackup -StartMenuBinFile $startMenuBinFile -BackupFilePath $BackupFilePath
}
<#
.SYNOPSIS
Restores the start menu for all user profiles from a backup.
.DESCRIPTION
Iterates over every existing user profile and restores each user's
start2.bin from the latest backup in their LocalState folder. For the
Default user profile, removes the start2.bin file (which was previously
copied from a template) so that new profiles revert to the system
default start menu.
.PARAMETER BackupFilePath
Path to the backup file to restore from. If omitted, automatically
finds the latest Win11Debloat-StartBackup-*.bak in each user's
LocalState folder.
.EXAMPLE
Restore-StartMenuForAllUsers
.EXAMPLE
Restore-StartMenuForAllUsers -BackupFilePath "C:\Backups\Win11Debloat-StartBackup-20260101_120000.bak"
#>
function Restore-StartMenuForAllUsers {
param(
[string]$BackupFilePath
)
$userPathString = Get-UserDirectory -userName "*" -fileName "AppData\Local\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState"
$usersStartMenuPaths = Get-ChildItem -Path $userPathString -ErrorAction SilentlyContinue
$results = @()
Write-Host "Restoring start menu for all users from backup..."
foreach ($startMenuPath in $usersStartMenuPaths) {
$startMenuBinFile = Join-Path $startMenuPath.FullName 'start2.bin'
$results += Restore-StartMenuFromBackup -StartMenuBinFile $startMenuBinFile -BackupFilePath $BackupFilePath
}
$defaultStartMenuPath = Get-UserDirectory -userName "Default" -fileName "AppData\Local\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState" -exitIfPathNotFound $false
if (Test-Path $defaultStartMenuPath) {
$defaultStartMenuBinFile = Join-Path $defaultStartMenuPath 'start2.bin'
if (Test-Path -LiteralPath $defaultStartMenuBinFile) {
if ($script:Params.ContainsKey("WhatIf")) {
Write-Host "[WhatIf] Remove start2.bin for the default user profile" -ForegroundColor Cyan
$results += [PSCustomObject]@{
UserName = 'Default'
Result = $true
Message = '[WhatIf] Removed start2.bin for the default user profile.'
}
}
else {
try {
Remove-Item -LiteralPath $defaultStartMenuBinFile -Force
$results += [PSCustomObject]@{
UserName = 'Default'
Result = $true
Message = 'Removed start2.bin for the default user profile.'
}
}
catch {
$results += [PSCustomObject]@{
UserName = 'Default'
Result = $false
Message = "Failed to remove start2.bin for the default user profile. $($_.Exception.Message)"
}
}
}
}
}
if ($results.Count -eq 0) {
$results += [PSCustomObject]@{
UserName = 'unknown'
Result = $false
Message = 'No user start menu locations were found.'
}
}
return $results
}
-221
View File
@@ -1,221 +0,0 @@
# Replace the startmenu for all users, when using the default startmenuTemplate this clears all pinned apps
# Credit: https://lazyadmin.nl/win-11/customize-windows-11-start-menu-layout/
function ReplaceStartMenuForAllUsers {
param (
$startMenuTemplate = "$script:AssetsPath\Start\start2.bin"
)
Write-Host "> Removing all pinned apps from the start menu for all users..."
# Check if template bin file exists
if (-not (Test-Path $startMenuTemplate)) {
Write-Host "Error: Unable to clear start menu, start2.bin file missing from script folder" -ForegroundColor Red
Write-Host ""
return
}
# Get path to start menu file for all users
$userPathString = GetUserDirectory -userName "*" -fileName "AppData\Local\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState"
$usersStartMenuPaths = Get-ChildItem -Path $userPathString -ErrorAction SilentlyContinue
# Go through all users and replace the start menu file
ForEach ($startMenuPath in $usersStartMenuPaths) {
ReplaceStartMenu $startMenuTemplate "$($startMenuPath.Fullname)\start2.bin"
}
# Also replace the start menu file for the default user profile
$defaultStartMenuPath = GetUserDirectory -userName "Default" -fileName "AppData\Local\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState" -exitIfPathNotFound $false
# Create folder if it doesn't exist
if (-not (Test-Path $defaultStartMenuPath)) {
new-item $defaultStartMenuPath -ItemType Directory -Force | Out-Null
Write-Host "Created LocalState folder for default user profile"
}
# Copy template to default profile
Copy-Item -Path $startMenuTemplate -Destination $defaultStartMenuPath -Force
Write-Host "Replaced start menu for the default user profile"
Write-Host ""
}
# Replace the startmenu at the specified location, when using the default startmenuTemplate this clears all pinned apps
# Credit: https://lazyadmin.nl/win-11/customize-windows-11-start-menu-layout/
function ReplaceStartMenu {
param (
$startMenuTemplate = "$script:AssetsPath\Start\start2.bin",
$startMenuBinFile = "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin"
)
# Change path to correct user if a user was specified
if ($script:Params.ContainsKey("User")) {
$startMenuBinFile = GetStartMenuBinPathForUser -UserName (GetUserName)
}
# Check if template bin file exists
if (-not (Test-Path $startMenuTemplate)) {
Write-Host "Error: Unable to replace start menu, template file not found" -ForegroundColor Red
return
}
if ([IO.Path]::GetExtension($startMenuTemplate) -ne ".bin" ) {
Write-Host "Error: Unable to replace start menu, template file is not a valid .bin file" -ForegroundColor Red
return
}
$userName = GetStartMenuUserNameFromPath -StartMenuBinFile $startMenuBinFile
$backupBinFile = $startMenuBinFile + ".bak"
if (Test-Path $startMenuBinFile) {
# Backup current start menu file
Move-Item -Path $startMenuBinFile -Destination $backupBinFile -Force
}
else {
Write-Host "Unable to find original start2.bin file for user $userName, no backup was created for this user" -ForegroundColor Yellow
New-Item -ItemType File -Path $startMenuBinFile -Force
}
# Copy template file
Copy-Item -Path $startMenuTemplate -Destination $startMenuBinFile -Force
Write-Host "Replaced start menu for user $userName"
}
function GetStartMenuBinPathForUser {
param(
[string]$UserName
)
if ([string]::IsNullOrWhiteSpace($UserName)) {
return "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin"
}
return (GetUserDirectory -userName $UserName -fileName "AppData\Local\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin" -exitIfPathNotFound $false)
}
function GetStartMenuUserNameFromPath {
param(
[string]$StartMenuBinFile
)
$resolvedUserName = [regex]::Match($StartMenuBinFile, '(?:Users\\)([^\\]+)(?:\\AppData)').Groups[1].Value
if ([string]::IsNullOrWhiteSpace($resolvedUserName)) {
return 'unknown'
}
return $resolvedUserName
}
function RestoreStartMenuFromBackup {
param(
[Parameter(Mandatory)]
[string]$StartMenuBinFile,
[Parameter(Mandatory = $false)]
[string]$BackupFilePath
)
$userName = GetStartMenuUserNameFromPath -StartMenuBinFile $StartMenuBinFile
$backupBinFile = if ([string]::IsNullOrWhiteSpace($BackupFilePath)) {
$StartMenuBinFile + '.bak'
}
else {
$BackupFilePath
}
$currentBinBackup = $StartMenuBinFile + '.restore.bak'
if (-not (Test-Path -LiteralPath $backupBinFile)) {
return [PSCustomObject]@{
UserName = $userName
Result = $false
Message = "No start menu backup file found for user $userName."
}
}
try {
if (Test-Path -LiteralPath $StartMenuBinFile) {
Move-Item -Path $StartMenuBinFile -Destination $currentBinBackup -Force
}
Copy-Item -Path $backupBinFile -Destination $StartMenuBinFile -Force
return [PSCustomObject]@{
UserName = $userName
Result = $true
Message = "Restored start menu for user $userName."
}
}
catch {
return [PSCustomObject]@{
UserName = $userName
Result = $false
Message = "Failed to restore start menu for user $userName. $($_.Exception.Message)"
}
}
}
function RestoreStartMenu {
param(
[Parameter(Mandatory = $false)]
[string]$BackupFilePath
)
$targetUserName = GetUserName
$startMenuBinFile = GetStartMenuBinPathForUser -UserName $targetUserName
Write-Host "Restoring start menu for user $targetUserName from backup..."
return RestoreStartMenuFromBackup -StartMenuBinFile $startMenuBinFile -BackupFilePath $BackupFilePath
}
function RestoreStartMenuForAllUsers {
param(
[Parameter(Mandatory = $false)]
[string]$BackupFilePath
)
$userPathString = GetUserDirectory -userName "*" -fileName "AppData\Local\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState"
$usersStartMenuPaths = Get-ChildItem -Path $userPathString -ErrorAction SilentlyContinue
$results = @()
Write-Host "Restoring start menu for all users from backup..."
foreach ($startMenuPath in $usersStartMenuPaths) {
$startMenuBinFile = Join-Path $startMenuPath.FullName 'start2.bin'
$results += RestoreStartMenuFromBackup -StartMenuBinFile $startMenuBinFile -BackupFilePath $BackupFilePath
}
$defaultStartMenuPath = GetUserDirectory -userName "Default" -fileName "AppData\Local\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState" -exitIfPathNotFound $false
if (Test-Path $defaultStartMenuPath) {
$defaultStartMenuBinFile = Join-Path $defaultStartMenuPath 'start2.bin'
if (Test-Path -LiteralPath $defaultStartMenuBinFile) {
try {
Remove-Item -LiteralPath $defaultStartMenuBinFile -Force
$results += [PSCustomObject]@{
UserName = 'Default'
Result = $true
Message = 'Removed start2.bin for the default user profile.'
}
}
catch {
$results += [PSCustomObject]@{
UserName = 'Default'
Result = $false
Message = "Failed to remove start2.bin for the default user profile. $($_.Exception.Message)"
}
}
}
}
if ($results.Count -eq 0) {
$results += [PSCustomObject]@{
UserName = 'unknown'
Result = $false
Message = 'No user start menu locations were found.'
}
}
return $results
}
@@ -0,0 +1,353 @@
<#
.SYNOPSIS
Runs a script block against the registry hive for a backup target.
.PARAMETER Target
A supported backup target: DefaultUserProfile or User:<user name>.
.PARAMETER ScriptBlock
The operation to run after the target user hive is available.
.PARAMETER ArgumentObject
Optional object passed to the script block.
#>
function Invoke-WithLoadedRestoreHive {
param(
[Parameter(Mandatory)]
[string]$Target,
[Parameter(Mandatory)]
[scriptblock]$ScriptBlock,
$ArgumentObject = $null
)
$targetUserName = if ($Target -eq 'DefaultUserProfile') {
'Default'
}
elseif ($Target -like 'User:*') {
$userName = $Target.Substring(5)
if ([string]::IsNullOrWhiteSpace($userName)) {
throw 'Invalid backup target format for user restore.'
}
$userName
}
else {
throw "Unsupported backup target '$Target'."
}
Invoke-WithTargetUserHive -TargetUserName $targetUserName -ScriptBlock $ScriptBlock -ArgumentObject $ArgumentObject
}
<#
.SYNOPSIS
Restores a registry key and its child keys from a backup snapshot.
.PARAMETER Snapshot
The saved registry-key state, including existence, values, and subkeys.
#>
function Restore-RegistryKeySnapshot {
param(
[Parameter(Mandatory)]
$Snapshot
)
$registryParts = Split-RegistryPath -path $Snapshot.Path
if (-not $registryParts) {
throw "Unsupported registry path in backup: $($Snapshot.Path)"
}
$rootKey = Get-RegistryRootKey -hiveName $registryParts.Hive
if (-not $rootKey) {
throw "Unsupported registry hive in backup: $($registryParts.Hive)"
}
$subKeyPath = $registryParts.SubKey
if ([string]::IsNullOrWhiteSpace($subKeyPath)) {
throw "Unsupported root-level registry path in backup: $($Snapshot.Path)"
}
Test-RegistryKeySnapshotCanBeRestored -Snapshot $Snapshot
Restore-RegistryKeySnapshotAtPath -Snapshot $Snapshot -RootKey $rootKey -SubKeyPath $subKeyPath
}
<#
.SYNOPSIS
Validates registry values and subkey paths in a snapshot before live registry state is changed.
.PARAMETER Snapshot
The registry key snapshot to validate before it is restored.
#>
function Test-RegistryKeySnapshotCanBeRestored {
param(
[Parameter(Mandatory)]
$Snapshot
)
if (-not [bool]$Snapshot.Exists) { return }
$childNames = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
foreach ($valueSnapshot in @($Snapshot.Values)) {
if ([bool]$valueSnapshot.Exists) {
$valueKind = Convert-RegistryValueKindFromBackup -KindName $valueSnapshot.Kind
$null = Convert-RegistryValueDataFromBackup -Kind $valueKind -Data $valueSnapshot.Data
}
}
foreach ($subKeySnapshot in @($Snapshot.SubKeys)) {
$childName = Get-DirectRegistrySnapshotChildName -ParentPath $Snapshot.Path -ChildPath $subKeySnapshot.Path
if ([string]::IsNullOrWhiteSpace($childName) -or -not $childNames.Add($childName)) {
throw "Backup contains duplicate or unsupported registry child path: $($subKeySnapshot.Path)"
}
Test-RegistryKeySnapshotCanBeRestored -Snapshot $subKeySnapshot
}
}
<#
.SYNOPSIS
Returns a snapshot child's name only when it is directly below its parent.
.PARAMETER ParentPath
The registry path of the expected parent snapshot.
.PARAMETER ChildPath
The registry path of the child snapshot to validate.
#>
function Get-DirectRegistrySnapshotChildName {
param(
[Parameter(Mandatory)]
[string]$ParentPath,
[Parameter(Mandatory)]
[string]$ChildPath
)
$parentParts = Split-RegistryPath -path $ParentPath
$childParts = Split-RegistryPath -path $ChildPath
if (-not $parentParts -or -not $childParts -or
-not $parentParts.Hive.Equals($childParts.Hive, [System.StringComparison]::OrdinalIgnoreCase) -or
[string]::IsNullOrWhiteSpace($parentParts.SubKey) -or
[string]::IsNullOrWhiteSpace($childParts.SubKey)) {
throw "Unsupported registry child path in backup: $ChildPath"
}
$childName = Split-Path -Path $childParts.SubKey -Leaf
$expectedSubKey = "$($parentParts.SubKey)\$childName"
if ([string]::IsNullOrWhiteSpace($childName) -or
-not $childParts.SubKey.Equals($expectedSubKey, [System.StringComparison]::OrdinalIgnoreCase)) {
throw "Registry child path '$ChildPath' is not directly below parent '$ParentPath'."
}
return $childName
}
<#
.SYNOPSIS
Restores a snapshot to a specific path below an already resolved registry root.
.DESCRIPTION
Writes only values and descendants represented by the backup. Existing keys are
retained so their security descriptors and unrelated data are not destroyed.
#>
function Restore-RegistryKeySnapshotAtPath {
param(
[Parameter(Mandatory)]
$Snapshot,
[Parameter(Mandatory)]
$RootKey,
[Parameter(Mandatory)]
[string]$SubKeyPath
)
if (-not $Snapshot.Exists) {
Remove-RegistrySubKeyTreeIfExists -RootKey $RootKey -SubKeyPath $SubKeyPath
return
}
$key = $RootKey.CreateSubKey($SubKeyPath)
if ($null -eq $key) {
throw "Unable to create or open registry key '$($Snapshot.Path)'"
}
try {
foreach ($valueSnapshot in @($Snapshot.Values)) {
Restore-RegistryValueSnapshot -RegistryKey $key -Snapshot $valueSnapshot
}
}
finally {
$key.Close()
}
foreach ($subKeySnapshot in @($Snapshot.SubKeys)) {
$childName = Get-DirectRegistrySnapshotChildName -ParentPath $Snapshot.Path -ChildPath $subKeySnapshot.Path
Restore-RegistryKeySnapshotAtPath -Snapshot $subKeySnapshot -RootKey $RootKey -SubKeyPath "$SubKeyPath\$childName"
}
}
<#
.SYNOPSIS
Restores or removes a registry value from a backup snapshot.
.PARAMETER RegistryKey
The open registry key that contains the value.
.PARAMETER Snapshot
The saved registry-value state to apply.
#>
function Restore-RegistryValueSnapshot {
param(
[Parameter(Mandatory)]
$RegistryKey,
[Parameter(Mandatory)]
$Snapshot
)
$valueName = if ($null -ne $Snapshot.Name) { [string]$Snapshot.Name } else { '' }
if (-not [bool]$Snapshot.Exists) {
try {
$RegistryKey.DeleteValue($valueName, $false)
}
catch {
throw "Failed deleting registry value '$valueName' in '$($RegistryKey.Name)': $($_.Exception.Message)"
}
return
}
$valueKind = Convert-RegistryValueKindFromBackup -KindName $Snapshot.Kind
$normalizedData = Convert-RegistryValueDataFromBackup -Kind $valueKind -Data $Snapshot.Data
try {
$RegistryKey.SetValue($valueName, $normalizedData, $valueKind)
}
catch {
throw "Failed setting registry value '$valueName' in '$($RegistryKey.Name)': $($_.Exception.Message)"
}
}
<#
.SYNOPSIS
Converts a backed-up registry value-kind name to its .NET enum value.
.PARAMETER KindName
The registry value-kind name stored in the backup.
.OUTPUTS
Microsoft.Win32.RegistryValueKind
#>
function Convert-RegistryValueKindFromBackup {
param(
[string]$KindName
)
if ([string]::IsNullOrWhiteSpace($KindName)) {
return [Microsoft.Win32.RegistryValueKind]::String
}
try {
return [System.Enum]::Parse([Microsoft.Win32.RegistryValueKind], $KindName, $true)
}
catch {
throw "Unsupported registry value kind in backup: $KindName"
}
}
<#
.SYNOPSIS
Converts backed-up data to a value suitable for registry restoration.
.PARAMETER Kind
The registry value kind that determines how the data is converted.
.PARAMETER Data
The serialized value data from the backup.
#>
function Convert-RegistryValueDataFromBackup {
param(
[Microsoft.Win32.RegistryValueKind]$Kind,
$Data
)
switch ($Kind) {
([Microsoft.Win32.RegistryValueKind]::DWord) {
$unsigned = [uint32]$Data
return [BitConverter]::ToInt32([BitConverter]::GetBytes($unsigned), 0)
}
([Microsoft.Win32.RegistryValueKind]::QWord) {
$unsigned = [uint64]$Data
return [BitConverter]::ToInt64([BitConverter]::GetBytes($unsigned), 0)
}
([Microsoft.Win32.RegistryValueKind]::MultiString) { return ,([string[]]@($Data | ForEach-Object { [string]$_ })) }
([Microsoft.Win32.RegistryValueKind]::Binary) {
if ($null -eq $Data) {
return ,(New-Object byte[] 0)
}
$bytes = Convert-BackupDataToByteArray -Data $Data
if ($null -eq $bytes) {
throw 'Invalid binary registry data in backup. Expected byte values from 0 through 255.'
}
# Keep the byte array intact instead of writing each byte to the
# pipeline. RegistryKey.SetValue requires a byte[] for Binary.
return ,$bytes
}
default {
if ($null -ne $Data) {
return [string]$Data
}
return ''
}
}
}
<#
.SYNOPSIS
Converts serialized binary backup data to a byte array.
.PARAMETER Data
A byte array or collection of integer byte values from the backup.
.OUTPUTS
System.Byte[]
Returns $null when the input contains invalid byte data.
#>
function Convert-BackupDataToByteArray {
param(
$Data
)
if ($null -eq $Data) {
return $null
}
if ($Data -is [byte[]]) {
return ,$Data
}
$items = @($Data)
if ($items.Count -eq 0) {
return ,(New-Object byte[] 0)
}
foreach ($item in $items) {
if ($item -isnot [ValueType] -and $item -isnot [string]) {
return $null
}
$parsed = 0
if (-not [int]::TryParse([string]$item, [ref]$parsed)) {
return $null
}
if ($parsed -lt 0 -or $parsed -gt 255) {
return $null
}
}
$bytes = New-Object byte[] $items.Count
for ($i = 0; $i -lt $items.Count; $i++) {
$bytes[$i] = [byte][int]$items[$i]
}
return ,$bytes
}
@@ -1,4 +1,20 @@
function Load-RegistryBackupFromFile {
<#
.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 ConvertTo-NormalizedRegistryBackup.
#>
function Import-RegistryBackup {
param(
[Parameter(Mandatory)]
[string]$FilePath
@@ -15,10 +31,28 @@ function Load-RegistryBackupFromFile {
throw "Failed to read backup file '$FilePath'. The file is not valid JSON."
}
return Normalize-RegistryBackup -Backup $rawBackup
return ConvertTo-NormalizedRegistryBackup -Backup $rawBackup
}
function Normalize-RegistryBackup {
<#
.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 ConvertTo-NormalizedRegistryBackup {
param(
[Parameter(Mandatory)]
$Backup
@@ -59,7 +93,11 @@ function Normalize-RegistryBackup {
}
elseif ($normalizedTarget -like 'CurrentUser:*') {
$targetCurrentUserName = $normalizedTarget.Substring(12)
if ([string]::IsNullOrWhiteSpace($targetCurrentUserName) -or ($targetCurrentUserName -ne $env:USERNAME)) {
if (Test-RunningAsSystem) {
$errors.Add("Backup was made for '$targetCurrentUserName' and is user-scoped. Re-run as that user; SYSTEM cannot restore a CurrentUser backup.")
}
elseif ([string]::IsNullOrWhiteSpace($targetCurrentUserName) -or
-not (Test-UserNameMatch -UserNameA $targetCurrentUserName -UserNameB $env:USERNAME)) {
$errors.Add("Backup was made for '$targetCurrentUserName', this does not match current user '$env:USERNAME'.")
}
}
@@ -97,9 +135,16 @@ function Normalize-RegistryBackup {
if ($allSelectedFeatures.Count -eq 0) {
$errors.Add('Backup must contain at least one feature ID in SelectedFeatures or SelectedUndoFeatures.')
}
$allowListValidationErrors = @(Test-RegistryBackupMatchesSelectedFeatures -SelectedFeatureIds @($selectedFeatures) -SelectedUndoFeatureIds @($selectedUndoFeatures) -Target $normalizedTarget -RegistryKeys @($normalizedKeys))
foreach ($allowListValidationError in $allowListValidationErrors) {
$errors.Add([string]$allowListValidationError)
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) {
@@ -125,13 +170,35 @@ 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 ConvertTo-NormalizedRegistryBackup) 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)]
$Backup
)
$friendlyTarget = GetFriendlyRegistryBackupTarget -Target ([string]$Backup.Target)
$friendlyTarget = Get-FriendlyRegistryBackupTarget -Target ([string]$Backup.Target)
if ($script:Params.ContainsKey("WhatIf")) {
Write-Host "[WhatIf] Restore registry backup for $friendlyTarget" -ForegroundColor Cyan
return [PSCustomObject]@{ Result = $true }
}
$restoreAction = {
param($normalizedBackup)
@@ -148,9 +215,10 @@ function Restore-RegistryBackupState {
Write-Host "Restore requires loading target user hive."
Invoke-WithLoadedRestoreHive -Target $Backup.Target -ScriptBlock $restoreAction -ArgumentObject $Backup
Write-Host "Restore completed for $friendlyTarget."
return
return [PSCustomObject]@{ Result = $true }
}
& $restoreAction $Backup
Write-Host "Restore completed for $friendlyTarget."
return [PSCustomObject]@{ Result = $true }
}
@@ -1,224 +0,0 @@
function Invoke-WithLoadedRestoreHive {
param(
[Parameter(Mandatory)]
[string]$Target,
[Parameter(Mandatory)]
[scriptblock]$ScriptBlock,
$ArgumentObject = $null
)
$targetUserName = if ($Target -eq 'DefaultUserProfile') {
'Default'
}
elseif ($Target -like 'User:*') {
$userName = $Target.Substring(5)
if ([string]::IsNullOrWhiteSpace($userName)) {
throw 'Invalid backup target format for user restore.'
}
$userName
}
else {
throw "Unsupported backup target '$Target'."
}
Invoke-WithTargetUserHive -TargetUserName $targetUserName -ScriptBlock $ScriptBlock -ArgumentObject $ArgumentObject
}
function Restore-RegistryKeySnapshot {
param(
[Parameter(Mandatory)]
$Snapshot
)
$registryParts = Split-RegistryPath -path $Snapshot.Path
if (-not $registryParts) {
throw "Unsupported registry path in backup: $($Snapshot.Path)"
}
$rootKey = Get-RegistryRootKey -hiveName $registryParts.Hive
if (-not $rootKey) {
throw "Unsupported registry hive in backup: $($registryParts.Hive)"
}
$subKeyPath = $registryParts.SubKey
if ([string]::IsNullOrWhiteSpace($subKeyPath)) {
throw "Unsupported root-level registry path in backup: $($Snapshot.Path)"
}
if (-not $Snapshot.Exists) {
Remove-RegistrySubKeyTreeIfExists -RootKey $rootKey -SubKeyPath $subKeyPath
return
}
$forceFullTree = @($Snapshot.SubKeys).Count -gt 0
if ($forceFullTree) {
Remove-RegistrySubKeyTreeIfExists -RootKey $rootKey -SubKeyPath $subKeyPath
}
$key = $rootKey.CreateSubKey($subKeyPath)
if ($null -eq $key) {
throw "Unable to create or open registry key '$($Snapshot.Path)'"
}
try {
foreach ($valueSnapshot in @($Snapshot.Values)) {
Restore-RegistryValueSnapshot -RegistryKey $key -Snapshot $valueSnapshot
}
}
finally {
$key.Close()
}
foreach ($subKeySnapshot in @($Snapshot.SubKeys)) {
Restore-RegistryKeySnapshot -Snapshot $subKeySnapshot
}
}
function Restore-RegistryValueSnapshot {
param(
[Parameter(Mandatory)]
[Microsoft.Win32.RegistryKey]$RegistryKey,
[Parameter(Mandatory)]
$Snapshot
)
$valueName = if ($null -ne $Snapshot.Name) { [string]$Snapshot.Name } else { '' }
if (-not [bool]$Snapshot.Exists) {
try {
$RegistryKey.DeleteValue($valueName, $false)
}
catch {
throw "Failed deleting registry value '$valueName' in '$($RegistryKey.Name)': $($_.Exception.Message)"
}
return
}
$valueKind = Convert-RegistryValueKindFromBackup -KindName $Snapshot.Kind
$normalizedData = Convert-RegistryValueDataFromBackup -Kind $valueKind -Data $Snapshot.Data
try {
$RegistryKey.SetValue($valueName, $normalizedData, $valueKind)
}
catch {
$retryBytes = Convert-BackupDataToByteArray -Data $Snapshot.Data
if ($null -ne $retryBytes) {
try {
$RegistryKey.SetValue($valueName, $retryBytes, [Microsoft.Win32.RegistryValueKind]::Binary)
return
}
catch {
# Fall through to original error message for context.
}
}
throw "Failed setting registry value '$valueName' in '$($RegistryKey.Name)': $($_.Exception.Message)"
}
}
function Convert-RegistryValueKindFromBackup {
param(
[string]$KindName
)
if ([string]::IsNullOrWhiteSpace($KindName)) {
return [Microsoft.Win32.RegistryValueKind]::String
}
try {
return [System.Enum]::Parse([Microsoft.Win32.RegistryValueKind], $KindName, $true)
}
catch {
throw "Unsupported registry value kind in backup: $KindName"
}
}
function Convert-RegistryValueDataFromBackup {
param(
[Microsoft.Win32.RegistryValueKind]$Kind,
$Data
)
switch ($Kind) {
([Microsoft.Win32.RegistryValueKind]::DWord) {
$unsigned = [uint32]$Data
return [BitConverter]::ToInt32([BitConverter]::GetBytes($unsigned), 0)
}
([Microsoft.Win32.RegistryValueKind]::QWord) {
$unsigned = [uint64]$Data
return [BitConverter]::ToInt64([BitConverter]::GetBytes($unsigned), 0)
}
([Microsoft.Win32.RegistryValueKind]::MultiString) { return @($Data | ForEach-Object { [string]$_ }) }
([Microsoft.Win32.RegistryValueKind]::Binary) {
$bytes = Convert-BackupDataToByteArray -Data $Data
if ($null -eq $bytes) {
return (New-Object byte[] 0)
}
return $bytes
}
([Microsoft.Win32.RegistryValueKind]::None) { return $null }
default {
if ($null -ne $Data) {
return [string]$Data
}
return ''
}
}
}
function Convert-BackupDataToByteArray {
param(
$Data
)
if ($null -eq $Data) {
return $null
}
if ($Data -is [byte[]]) {
return ,$Data
}
$items = @($Data)
if ($items.Count -eq 0) {
return ,(New-Object byte[] 0)
}
foreach ($item in $items) {
if ($item -isnot [ValueType] -and $item -isnot [string]) {
return $null
}
$parsed = 0
if (-not [int]::TryParse([string]$item, [ref]$parsed)) {
return $null
}
if ($parsed -lt 0 -or $parsed -gt 255) {
return $null
}
}
$bytes = New-Object byte[] $items.Count
for ($i = 0; $i -lt $items.Count; $i++) {
$bytes[$i] = [byte][int]$items[$i]
}
return ,$bytes
}
function Remove-RegistrySubKeyTreeIfExists {
param(
[Parameter(Mandatory)]
[Microsoft.Win32.RegistryKey]$RootKey,
[Parameter(Mandatory)]
[string]$SubKeyPath
)
$existing = $RootKey.OpenSubKey($SubKeyPath, $false)
if ($existing) {
$existing.Close()
$RootKey.DeleteSubKeyTree($SubKeyPath, $false)
}
}
@@ -0,0 +1,371 @@
<#
.SYNOPSIS
Disables Microsoft Store search suggestions in the start menu for all user profiles.
.DESCRIPTION
Iterates over every existing user profile and the Default user profile,
denying the EVERYONE group FullControl access to each user's Store app
database file (store.db). This prevents Windows from showing Store search
suggestions in the start menu search pane.
.EXAMPLE
DisableStoreSearchSuggestionsForAllUsers
.OUTPUTS
System.Boolean. $true when a profile is processed and all ACL changes succeed; otherwise $false.
#>
function Set-StoreSearchSuggestionsDisabledForAllUsers {
$success = $true
$processedProfiles = 0
# Get path to Store app database for all users
$userPathString = Get-UserDirectory -userName "*" -fileName "AppData\Local\Packages"
$usersStoreDbPaths = Get-ChildItem -Path $userPathString -ErrorAction SilentlyContinue
# Go through all users and disable start search suggestions
foreach ($storeDbPath in $usersStoreDbPaths) {
$processedProfiles++
if (-not (Set-StoreSearchSuggestionsDisabled -StoreAppsDatabase ($storeDbPath.FullName + "\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db"))) {
$success = $false
}
}
# Also disable start search suggestions for the default user profile
$defaultStoreDbPath = Get-StoreAppsDatabasePathForUser -UserName "Default"
if ($defaultStoreDbPath) {
$processedProfiles++
if (-not (Set-StoreSearchSuggestionsDisabled -StoreAppsDatabase $defaultStoreDbPath)) {
$success = $false
}
}
if ($processedProfiles -eq 0) {
Write-Warning 'Unable to disable Microsoft Store search suggestions because no target user profiles could be resolved.'
return $false
}
return $success
}
<#
.SYNOPSIS
Disables Microsoft Store search suggestions for a single user.
.DESCRIPTION
Denies the EVERYONE group FullControl access to the specified Store app
database file (store.db). If the file does not exist (e.g. on EEA systems
where Store app suggestions are absent by default), it creates the file
and its parent directory first to prevent Windows from recreating it later.
.PARAMETER StoreAppsDatabase
The full path to the user's store.db file.
.EXAMPLE
DisableStoreSearchSuggestions -StoreAppsDatabase "$env:LOCALAPPDATA\Packages\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db"
.OUTPUTS
System.Boolean. $true when the database ACL is restricted or previewed; otherwise $false.
#>
function Set-StoreSearchSuggestionsDisabled {
param (
[Parameter(Mandatory)]
[string]$StoreAppsDatabase
)
$userName = [regex]::Match($StoreAppsDatabase, '(?:Users\\)([^\\]+)(?:\\AppData)').Groups[1].Value
if (-not $userName) { $userName = '<unknown>' }
if ($script:Params.ContainsKey("WhatIf")) {
Write-Host "[WhatIf] Disable Microsoft Store search suggestions for user $userName by restricting access to ${StoreAppsDatabase}" -ForegroundColor Cyan
return $true
}
try {
# This file doesn't exist in EEA (No Store app suggestions).
if (-not (Test-Path -Path $StoreAppsDatabase)) {
Write-Host "Unable to find Store app database for user $userName, creating it now to prevent Windows from creating it later..." -ForegroundColor Yellow
$storeDbDir = Split-Path -Path $StoreAppsDatabase -Parent
if (-not (Test-Path -Path $storeDbDir)) {
New-Item -Path $storeDbDir -ItemType Directory -Force -ErrorAction Stop | Out-Null
}
New-Item -Path $StoreAppsDatabase -ItemType File -Force -ErrorAction Stop | Out-Null
}
$AccountSid = [System.Security.Principal.SecurityIdentifier]::new('S-1-1-0') # 'EVERYONE' group
$Acl = Get-Acl -Path $StoreAppsDatabase -ErrorAction Stop
$Ace = [System.Security.AccessControl.FileSystemAccessRule]::new($AccountSid, 'FullControl', 'Deny')
$Acl.SetAccessRule($Ace) | Out-Null
Set-Acl -Path $StoreAppsDatabase -AclObject $Acl -ErrorAction Stop | Out-Null
}
catch {
Write-Warning "Failed to restrict ACL for store database '$StoreAppsDatabase': $($_.Exception.Message)"
return $false
}
Write-Host "Disabled Microsoft Store search suggestions for user $userName"
return $true
}
<#
.SYNOPSIS
Re-enables Microsoft Store search suggestions in the start menu for all user profiles.
.DESCRIPTION
Iterates over every existing user profile and the Default user profile,
removing the deny ACL from each user's Store app database file (store.db)
and then deleting the file. This restores the default Windows behavior
where Store search suggestions appear in the start menu.
.EXAMPLE
EnableStoreSearchSuggestionsForAllUsers
.OUTPUTS
System.Boolean. $true when a profile is processed and all ACL changes succeed; otherwise $false.
#>
function Set-StoreSearchSuggestionsEnabledForAllUsers {
$success = $true
$processedProfiles = 0
# Get path to Store app database for all users
$userPathString = Get-UserDirectory -userName "*" -fileName "AppData\Local\Packages"
$usersStoreDbPaths = Get-ChildItem -Path $userPathString -ErrorAction SilentlyContinue
# Go through all users and re-enable start search suggestions
foreach ($storeDbPath in $usersStoreDbPaths) {
$processedProfiles++
if (-not (Set-StoreSearchSuggestionsEnabled -StoreAppsDatabase ($storeDbPath.FullName + "\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db"))) {
$success = $false
}
}
# Also re-enable for the default user profile
$defaultStoreDbPath = Get-StoreAppsDatabasePathForUser -UserName "Default"
if ($defaultStoreDbPath) {
$processedProfiles++
if (-not (Set-StoreSearchSuggestionsEnabled -StoreAppsDatabase $defaultStoreDbPath)) {
$success = $false
}
}
if ($processedProfiles -eq 0) {
Write-Warning 'Unable to re-enable Microsoft Store search suggestions because no target user profiles could be resolved.'
return $false
}
return $success
}
<#
.SYNOPSIS
Re-enables Microsoft Store search suggestions for a single user.
.DESCRIPTION
Takes ownership of the specified Store app database file, removes any
EVERYONE deny FullControl ACL entries, and deletes the file. If the file
does not exist, no action is taken. Callers should handle the case where
the file is absent gracefully.
.PARAMETER StoreAppsDatabase
The full path to the user's store.db file.
.EXAMPLE
EnableStoreSearchSuggestions -StoreAppsDatabase "$env:LOCALAPPDATA\Packages\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db"
.OUTPUTS
System.Boolean. $true when the deny ACL is removed, the database is absent, or the change is previewed; otherwise $false.
#>
function Set-StoreSearchSuggestionsEnabled {
param (
[Parameter(Mandatory)]
[string]$StoreAppsDatabase
)
$userName = [regex]::Match($StoreAppsDatabase, '(?:Users\\)([^\\]+)(?:\\AppData)').Groups[1].Value
if (-not $userName) { $userName = '<unknown>' }
if ($script:Params.ContainsKey("WhatIf")) {
Write-Host "[WhatIf] Re-enable Microsoft Store search suggestions for user $userName by restoring access to ${StoreAppsDatabase}" -ForegroundColor Cyan
return $true
}
if (-not (Test-Path -Path $StoreAppsDatabase)) {
Write-Host "Store app database not found for user $userName, nothing to undo"
return $true
}
# Ensure we can modify/delete the file even if restrictive ACLs were set.
$global:LASTEXITCODE = 0
takeown /F "$StoreAppsDatabase" /A | Out-Null
if ($LASTEXITCODE -ne 0) {
Write-Warning "Failed to take ownership of store database '$StoreAppsDatabase' while undoing Microsoft Store search suggestions. Exit code: $LASTEXITCODE"
return $false
}
icacls "$StoreAppsDatabase" /grant *S-1-5-32-544:F /C | Out-Null
if ($LASTEXITCODE -ne 0) {
Write-Warning "Failed to grant Administrators access to store database '$StoreAppsDatabase' while undoing Microsoft Store search suggestions. Exit code: $LASTEXITCODE"
return $false
}
$everyoneSid = [System.Security.Principal.SecurityIdentifier]::new('S-1-1-0') # 'EVERYONE' group
try {
$acl = Get-Acl -Path $StoreAppsDatabase -ErrorAction Stop
$denyRules = @(
$acl.Access | Where-Object {
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
}
}
)
foreach ($denyRule in $denyRules) {
$null = $acl.RemoveAccessRuleSpecific($denyRule)
}
Set-Acl -Path $StoreAppsDatabase -AclObject $acl -ErrorAction Stop | Out-Null
}
catch {
Write-Warning "Failed to normalize ACL for store database '$StoreAppsDatabase': $($_.Exception.Message)"
}
try {
Remove-Item -Path $StoreAppsDatabase -Force -ErrorAction Stop
Write-Host "Re-enabled Microsoft Store search suggestions for user $userName"
return $true
}
catch {
Write-Warning "Failed to remove '$StoreAppsDatabase' while undoing Microsoft Store search suggestions for user $userName. $($_.Exception.Message)"
return $false
}
}
<#
.SYNOPSIS
Returns the full path to the Store app database file for a given user.
.DESCRIPTION
Resolves the path to the Microsoft Store app database (store.db) for the
specified username. When no username is provided or the value is empty,
falls back to the current user's local app data path via $env:LOCALAPPDATA.
.PARAMETER UserName
The target username. Pass an empty string or omit to resolve for the current user.
.EXAMPLE
Get-StoreAppsDatabasePathForUser -UserName "Jeff"
.EXAMPLE
Get-StoreAppsDatabasePathForUser -UserName "Default"
#>
function Get-StoreAppsDatabasePathForUser {
param(
[string]$UserName
)
if ([string]::IsNullOrWhiteSpace($UserName)) {
return "$env:LOCALAPPDATA\Packages\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db"
}
return (Get-UserDirectory -userName $UserName -fileName "AppData\Local\Packages\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db" -exitIfPathNotFound $false)
}
<#
.SYNOPSIS
Tests whether Store search suggestions are disabled for a single user.
.DESCRIPTION
Checks whether the specified store.db file has an EVERYONE deny
FullControl ACL entry applied. Returns $true if the deny rule is present,
$false otherwise (including when the file or directory does not exist).
.PARAMETER StoreAppsDatabase
The full path to the user's store.db file.
.EXAMPLE
Test-StoreSearchSuggestionsDisabled -StoreAppsDatabase "C:\Users\Jeff\AppData\Local\Packages\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db"
#>
function Test-StoreSearchSuggestionsDisabled {
param(
[Parameter(Mandatory)]
[string]$StoreAppsDatabase
)
if (-not (Test-Path -Path $StoreAppsDatabase)) {
return $false
}
try {
$acl = Get-Acl -Path $StoreAppsDatabase
}
catch {
return $false
}
$everyoneSid = [System.Security.Principal.SecurityIdentifier]::new('S-1-1-0')
foreach ($accessRule in @($acl.Access)) {
$isDenyFullControl = $accessRule.AccessControlType -eq [System.Security.AccessControl.AccessControlType]::Deny -and
(($accessRule.FileSystemRights -band [System.Security.AccessControl.FileSystemRights]::FullControl) -ne 0)
if (-not $isDenyFullControl) { continue }
$isEveryone = $false
try {
$isEveryone = $accessRule.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]) -eq $everyoneSid
}
catch { }
if ($isEveryone) {
return $true
}
}
return $false
}
<#
.SYNOPSIS
Tests whether Store search suggestions are disabled for all user profiles.
.DESCRIPTION
Collects the store.db paths for all existing user profiles and the Default
user profile, then verifies that every one of them has the EVERYONE deny
FullControl ACL applied. Returns $true only if ALL paths pass the check.
Returns $false immediately if any user's store.db is not disabled.
.EXAMPLE
Test-StoreSearchSuggestionsDisabledForAllUsers
#>
function Test-StoreSearchSuggestionsDisabledForAllUsers {
$paths = @()
$userPathString = Get-UserDirectory -userName "*" -fileName "AppData\Local\Packages"
$usersStoreDbPaths = Get-ChildItem -Path $userPathString -ErrorAction SilentlyContinue
foreach ($storeDbPath in $usersStoreDbPaths) {
$paths += ($storeDbPath.FullName + "\Microsoft.WindowsStore_8wekyb3d8bbwe\LocalState\store.db")
}
$defaultStoreDbPath = Get-StoreAppsDatabasePathForUser -UserName "Default"
if ($defaultStoreDbPath) {
$paths += $defaultStoreDbPath
}
if ($paths.Count -eq 0) {
return $false
}
foreach ($path in $paths) {
if (-not (Test-StoreSearchSuggestionsDisabled -StoreAppsDatabase $path)) {
return $false
}
}
return $true
}
@@ -0,0 +1,172 @@
# List of known Windows telemetry-related scheduled tasks
<#
.SYNOPSIS
Returns the list of known Windows telemetry-related scheduled tasks.
.DESCRIPTION
Returns an array of hashtables, each with a Path and Name key, representing
scheduled tasks that collect or report telemetry data on Windows.
.EXAMPLE
Get-TelemetryScheduledTasks
#>
function Get-TelemetryScheduledTasks {
return @(
@{ Path = "\Microsoft\Windows\Application Experience\"; Name = "Microsoft Compatibility Appraiser" },
@{ Path = "\Microsoft\Windows\Application Experience\"; Name = "Microsoft Compatibility Appraiser Exp" },
@{ Path = "\Microsoft\Windows\Application Experience\"; Name = "ProgramDataUpdater" },
@{ Path = "\Microsoft\Windows\Application Experience\"; Name = "StartupAppTask" },
@{ Path = "\Microsoft\Windows\Customer Experience Improvement Program\"; Name = "Consolidator" },
@{ Path = "\Microsoft\Windows\Customer Experience Improvement Program\"; Name = "UsbCeip" },
@{ Path = "\Microsoft\Windows\DiskDiagnostic\"; Name = "Microsoft-Windows-DiskDiagnosticDataCollector" },
@{ Path = "\Microsoft\Windows\Autochk\"; Name = "Proxy" }
)
}
<#
.SYNOPSIS
Disables known Windows telemetry-related scheduled tasks.
.DESCRIPTION
Iterates over a predefined list of Windows scheduled tasks associated with
telemetry and disables each one that exists and is not already disabled.
Supports -WhatIf to preview changes without applying them.
.EXAMPLE
Disable-TelemetryScheduledTasks
.OUTPUTS
System.Boolean. $true when every task is disabled, absent, already disabled, or previewed; otherwise $false.
#>
function Disable-TelemetryScheduledTasks {
Write-Host "> Disabling telemetry scheduled tasks..."
$tasks = Get-TelemetryScheduledTasks
$success = $true
foreach ($task in $tasks) {
if ($script:CancelRequested) { return $false }
if ($script:Params.ContainsKey("WhatIf")) {
Write-Host "[WhatIf] Disable Scheduled Task: $($task.Path)$($task.Name)" -ForegroundColor Cyan
continue
}
try {
$result = Invoke-NonBlocking -ScriptBlock {
param($path, $name)
try {
Import-Module ScheduledTasks -ErrorAction Stop
$taskObj = Get-ScheduledTask -TaskPath $path -TaskName $name -ErrorAction Stop
}
catch {
if ($_.Exception -isnot [System.Management.Automation.CommandNotFoundException] -and $_.CategoryInfo.Category -eq [System.Management.Automation.ErrorCategory]::ObjectNotFound) {
return @{ Success = $true; Status = 'NotFound' }
}
return @{ Success = $false; Status = 'Error'; Error = $_.Exception.Message }
}
if (-not $taskObj) {
return @{ Success = $true; Status = 'NotFound' }
}
if ($taskObj.State -ne 'Disabled') {
try {
Disable-ScheduledTask -TaskPath $path -TaskName $name -ErrorAction Stop | Out-Null
return @{ Success = $true; Status = 'Disabled' }
}
catch {
return @{ Success = $false; Status = 'Error'; Error = $_.Exception.Message }
}
}
return @{ Success = $true; Status = 'AlreadyDisabled' }
} -ArgumentList @($task.Path, $task.Name)
}
catch {
Write-Warning "Failed to disable Scheduled Task: $($task.Path)$($task.Name) - $($_.Exception.Message)"
$success = $false
continue
}
switch ($result.Status) {
'Disabled' { Write-Host "Disabled Scheduled Task: $($task.Path)$($task.Name)" }
'AlreadyDisabled' { Write-Host "Scheduled Task $($task.Path)$($task.Name) is already disabled" -ForegroundColor DarkGray }
'NotFound' { Write-Host "Scheduled Task $($task.Path)$($task.Name) not found" -ForegroundColor DarkGray }
'Error' { Write-Host "Failed to disable Scheduled Task: $($task.Path)$($task.Name) - $($result.Error)" -ForegroundColor Yellow; $success = $false }
default { Write-Warning "Unable to determine the result of disabling Scheduled Task: $($task.Path)$($task.Name)."; $success = $false }
}
}
return $success
}
<#
.SYNOPSIS
Enables known Windows telemetry-related scheduled tasks.
.DESCRIPTION
Iterates over a predefined list of Windows scheduled tasks associated with
telemetry and enables each one that exists and is currently disabled.
Supports -WhatIf to preview changes without applying them.
.EXAMPLE
Enable-TelemetryScheduledTasks
.OUTPUTS
System.Boolean. $true when every task is enabled, absent, already enabled, or previewed; otherwise $false.
#>
function Enable-TelemetryScheduledTasks {
Write-Host "> Enabling telemetry scheduled tasks..."
$tasks = Get-TelemetryScheduledTasks
$success = $true
foreach ($task in $tasks) {
if ($script:CancelRequested) { return $false }
if ($script:Params.ContainsKey("WhatIf")) {
Write-Host "[WhatIf] Enable Scheduled Task: $($task.Path)$($task.Name)" -ForegroundColor Cyan
continue
}
try {
$result = Invoke-NonBlocking -ScriptBlock {
param($path, $name)
try {
Import-Module ScheduledTasks -ErrorAction Stop
$taskObj = Get-ScheduledTask -TaskPath $path -TaskName $name -ErrorAction Stop
}
catch {
if ($_.Exception -isnot [System.Management.Automation.CommandNotFoundException] -and $_.CategoryInfo.Category -eq [System.Management.Automation.ErrorCategory]::ObjectNotFound) {
return @{ Success = $true; Status = 'NotFound' }
}
return @{ Success = $false; Status = 'Error'; Error = $_.Exception.Message }
}
if (-not $taskObj) {
return @{ Success = $true; Status = 'NotFound' }
}
if ($taskObj.State -eq 'Disabled') {
try {
Enable-ScheduledTask -TaskPath $path -TaskName $name -ErrorAction Stop | Out-Null
return @{ Success = $true; Status = 'Enabled' }
}
catch {
return @{ Success = $false; Status = 'Error'; Error = $_.Exception.Message }
}
}
return @{ Success = $true; Status = 'AlreadyEnabled' }
} -ArgumentList @($task.Path, $task.Name)
}
catch {
Write-Warning "Failed to enable Scheduled Task: $($task.Path)$($task.Name) - $($_.Exception.Message)"
$success = $false
continue
}
switch ($result.Status) {
'Enabled' { Write-Host "Enabled Scheduled Task: $($task.Path)$($task.Name)" }
'AlreadyEnabled' { Write-Host "Scheduled Task $($task.Path)$($task.Name) is already enabled." -ForegroundColor DarkGray }
'NotFound' { Write-Host "Scheduled Task $($task.Path)$($task.Name) not found." -ForegroundColor DarkGray }
'Error' { Write-Host "Failed to enable Scheduled Task: $($task.Path)$($task.Name) - $($result.Error)" -ForegroundColor Yellow; $success = $false }
default { Write-Warning "Unable to determine the result of enabling Scheduled Task: $($task.Path)$($task.Name)."; $success = $false }
}
}
return $success
}
@@ -0,0 +1,119 @@
<#
.SYNOPSIS
Enables a Windows optional feature and pipes its output to the console.
.OUTPUTS
System.Boolean. $true when enabling succeeds or is previewed; otherwise $false.
#>
function Enable-WindowsFeature {
param (
[string]$FeatureName
)
if ($script:Params.ContainsKey("WhatIf")) {
Write-Host "[WhatIf] Enable Windows feature: $FeatureName" -ForegroundColor Cyan
return $true
}
try {
$result = Invoke-NonBlocking -ScriptBlock {
param($name)
try {
$output = Enable-WindowsOptionalFeature -Online -FeatureName $name -All -NoRestart -ErrorAction Stop
return [PSCustomObject]@{
Success = $true
Output = if ($output) { ($output | Out-String).Trim() } else { $null }
Error = $null
}
}
catch {
return [PSCustomObject]@{
Success = $false
Output = $null
Error = $_.Exception.Message
}
}
} -ArgumentList $FeatureName
}
catch {
Write-Warning "Failed to enable Windows feature '$FeatureName': $($_.Exception.Message)"
return $false
}
if (-not $result -or -not $result.Success) {
$details = if ($result -and $result.Error) { ": $($result.Error)" } else { '' }
Write-Warning "Failed to enable Windows feature '$FeatureName'$details"
return $false
}
if ($result.Output) { Write-Host $result.Output }
return $true
}
<#
.SYNOPSIS
Disables a Windows optional feature and pipes its output to the console.
.OUTPUTS
System.Boolean. $true when disabling succeeds or is previewed; otherwise $false.
#>
function Disable-WindowsFeature {
param (
[string]$FeatureName
)
if ($script:Params.ContainsKey("WhatIf")) {
Write-Host "[WhatIf] Disable Windows feature: $FeatureName" -ForegroundColor Cyan
return $true
}
try {
$result = Invoke-NonBlocking -ScriptBlock {
param($name)
try {
$output = Disable-WindowsOptionalFeature -Online -FeatureName $name -NoRestart -ErrorAction Stop
return [PSCustomObject]@{
Success = $true
Output = if ($output) { ($output | Out-String).Trim() } else { $null }
Error = $null
}
}
catch {
return [PSCustomObject]@{
Success = $false
Output = $null
Error = $_.Exception.Message
}
}
} -ArgumentList $FeatureName
}
catch {
Write-Warning "Failed to disable Windows feature '$FeatureName': $($_.Exception.Message)"
return $false
}
if (-not $result -or -not $result.Success) {
$details = if ($result -and $result.Error) { ": $($result.Error)" } else { '' }
Write-Warning "Failed to disable Windows feature '$FeatureName'$details"
return $false
}
if ($result.Output) { Write-Host $result.Output }
return $true
}
function Test-WindowsOptionalFeatureEnabled {
param (
[Parameter(Mandatory)]
[string]$FeatureName
)
try {
$feature = Get-WindowsOptionalFeature -Online -FeatureName $FeatureName -ErrorAction Stop
}
catch {
return $false
}
return ($feature.State -eq 'Enabled')
}
@@ -1,49 +0,0 @@
# Enables a Windows optional feature and pipes its output to the console
function EnableWindowsFeature {
param (
[string]$FeatureName
)
$result = Invoke-NonBlocking -ScriptBlock {
param($name)
Enable-WindowsOptionalFeature -Online -FeatureName $name -All -NoRestart
} -ArgumentList $FeatureName
$dismResult = @($result) | Where-Object { $_ -is [Microsoft.Dism.Commands.ImageObject] }
if ($dismResult) {
Write-Host ($dismResult | Out-String).Trim()
}
}
# Disables a Windows optional feature and pipes its output to the console
function DisableWindowsFeature {
param (
[string]$FeatureName
)
$result = Invoke-NonBlocking -ScriptBlock {
param($name)
Disable-WindowsOptionalFeature -Online -FeatureName $name -NoRestart
} -ArgumentList $FeatureName
$dismResult = @($result) | Where-Object { $_ -is [Microsoft.Dism.Commands.ImageObject] }
if ($dismResult) {
Write-Host ($dismResult | Out-String).Trim()
}
}
function Test-WindowsOptionalFeatureEnabled {
param (
[Parameter(Mandatory)]
[string]$FeatureName
)
try {
$feature = Get-WindowsOptionalFeature -Online -FeatureName $FeatureName -ErrorAction Stop
}
catch {
return $false
}
return ($feature.State -eq 'Enabled')
}
@@ -1,10 +1,10 @@
# Returns a validated list of apps based on the provided appsList and the supported apps from Apps.json
function ValidateAppslist {
function Get-ValidatedAppList {
param (
$appsList
)
$supportedAppsList = @(LoadAppsDetailsFromJson | ForEach-Object { @($_.AppId) }) | ForEach-Object { $_.Trim() } | Where-Object { $_.Length -gt 0 }
$supportedAppsList = @(Import-AppDetailsFromJson | ForEach-Object { @($_.AppId) }) | ForEach-Object { $_.Trim() } | Where-Object { $_.Length -gt 0 }
$validatedAppsList = @()
# Validate provided appsList against supportedAppsList
@@ -1,8 +1,30 @@
# Read Apps.json and return list of app objects with optional filtering
function LoadAppsDetailsFromJson {
<#
.SYNOPSIS
Loads application details from Apps.json.
.DESCRIPTION
Reads the application definitions from Apps.json, optionally filters the
results to installed applications, and returns normalized app objects for
display and selection.
.PARAMETER OnlyInstalled
Filters the results to applications detected through Appx or the supplied
winget installation list.
.PARAMETER InstalledList
A pre-fetched winget installation list used when filtering installed apps.
.PARAMETER InitialCheckedFromJson
Sets each returned app's IsChecked value from its SelectedByDefault setting.
.OUTPUTS
System.Management.Automation.PSCustomObject[]
Application detail objects containing display, selection, and removal data.
#>
function Import-AppDetailsFromJson {
param (
[switch]$OnlyInstalled,
[string]$InstalledList = "",
[object[]]$InstalledList = $null,
[switch]$InitialCheckedFromJson
)
@@ -17,22 +39,31 @@ function LoadAppsDetailsFromJson {
foreach ($appData in $jsonContent.Apps) {
# Handle AppId as array (could be single or multiple IDs)
$appIdArray = if ($appData.AppId -is [array]) { $appData.AppId } else { @($appData.AppId) }
$appIdArray = $appIdArray | ForEach-Object { $_.Trim() } | Where-Object { $_.length -gt 0 }
$appIdArray = @(
foreach ($rawAppId in @($appData.AppId)) {
if ($rawAppId -isnot [string]) { continue }
$normalizedAppId = $rawAppId.Trim()
if ($normalizedAppId.Length -gt 0) { $normalizedAppId }
}
)
if ($appIdArray.Count -eq 0) { continue }
if ($OnlyInstalled) {
$isInstalled = $false
foreach ($appId in $appIdArray) {
if (($InstalledList -like ("*$appId*")) -or (Get-AppxPackage -Name $appId)) {
# Check Get-AppxPackage first (fast, no process launch)
if (Get-AppxPackage -Name $appId) {
$isInstalled = $true
break
}
if (($appId -eq "Microsoft.Edge") -and ($InstalledList -like "* Microsoft.Edge *")) {
# Then check the pre-fetched winget list
if ($InstalledList -and (Test-AppInWingetList -appId $appId -InstalledList $InstalledList)) {
$isInstalled = $true
break
}
}
if (-not $isInstalled) { continue }
}
@@ -52,6 +83,7 @@ function LoadAppsDetailsFromJson {
Description = $appData.Description
SelectedByDefault = $appData.SelectedByDefault
Recommendation = $appData.Recommendation
RemovalMethod = if ($appData.RemovalMethod -and $appData.RemovalMethod -eq 'WinGet') { 'WinGet' } else { 'Appx' }
}
}
@@ -1,6 +1,8 @@
# Read Apps.json and return the list of preset objects (Name + AppIds).
# Returns an empty array if the file cannot be read or contains no presets.
function LoadAppPresetsFromJson {
<#
.SYNOPSIS
Returns preset names and application IDs from Apps.json, or an empty array when unavailable.
#>
function Import-AppPresetsFromJson {
try {
$jsonContent = Get-Content -Path $script:AppsListFilePath -Raw | ConvertFrom-Json
}
+46
View File
@@ -0,0 +1,46 @@
<#
.SYNOPSIS
Returns a list of app IDs from the specified JSON file.
.DESCRIPTION
Reads an Apps.json file and returns the AppIds for every entry where
SelectedByDefault is $true. Each app entry may declare a single AppId
or an array of AppIds; both forms are handled transparently.
.PARAMETER appsFilePath
Path to a JSON file in the Config/Apps.json format.
.OUTPUTS
System.String[]. An array of app ID strings, or an empty array if the
file does not exist or contains no selected-by-default apps.
#>
function Import-AppsFromFile {
param (
$appsFilePath
)
$appsList = @()
if (-not (Test-Path $appsFilePath)) {
return $appsList
}
try {
$jsonContent = Get-Content -Path $appsFilePath -Raw | ConvertFrom-Json
Foreach ($appData in $jsonContent.Apps) {
# Handle AppId as array (could be single or multiple IDs)
$appIdArray = if ($appData.AppId -is [array]) { $appData.AppId } else { @($appData.AppId) }
$appIdArray = $appIdArray | ForEach-Object { $_.Trim() } | Where-Object { $_.length -gt 0 }
$selectedByDefault = $appData.SelectedByDefault
if ($selectedByDefault -and $appIdArray.Count -gt 0) {
$appsList += $appIdArray
}
}
return $appsList
}
catch {
Write-Error "Unable to read apps list from file: $appsFilePath"
Wait-ForKeyPress -ExitCode 1
}
}
@@ -1,6 +1,8 @@
# Loads a JSON file from the specified path and returns the parsed object
# Returns $null if the file doesn't exist or if parsing fails
function LoadJsonFile {
<#
.SYNOPSIS
Imports a JSON file, optionally validates its version, and returns $null on failure.
#>
function Import-JsonFile {
param (
[string]$filePath,
[string]$expectedVersion = $null,
@@ -1,11 +1,14 @@
# Loads settings from a JSON file and adds them to script params
function LoadSettings {
<#
.SYNOPSIS
Imports enabled, compatible feature settings from a JSON file into the active parameters.
#>
function Import-Settings {
param (
[string]$filePath,
[string]$expectedVersion = "1.0"
)
$settingsJson = LoadJsonFile -filePath $filePath -expectedVersion $expectedVersion
$settingsJson = Import-JsonFile -filePath $filePath -expectedVersion $expectedVersion
if (-not $settingsJson -or -not $settingsJson.Settings) {
throw "Failed to load settings from $(Split-Path $filePath -Leaf)"
@@ -21,11 +24,14 @@ function LoadSettings {
$feature = $script:Features[$setting.Name]
# Skip unknown settings that aren't defined in Features.json
if (-not $feature) { continue }
# Check version and feature compatibility using Features.json
if (($feature.MinVersion -and $WinVersion -lt $feature.MinVersion) -or ($feature.MaxVersion -and $WinVersion -gt $feature.MaxVersion) -or ($feature.FeatureId -eq 'DisableModernStandbyNetworking' -and (-not $script:ModernStandbySupported))) {
continue
}
AddParameter $setting.Name $setting.Value
Add-Parameter $setting.Name $setting.Value
}
}
-47
View File
@@ -1,47 +0,0 @@
# Returns list of apps from the specified file, it trims the app names and removes any comments
function LoadAppsFromFile {
param (
$appsFilePath
)
$appsList = @()
if (-not (Test-Path $appsFilePath)) {
return $appsList
}
try {
# Check if file is JSON or text format
if ($appsFilePath -like "*.json") {
# JSON file format
$jsonContent = Get-Content -Path $appsFilePath -Raw | ConvertFrom-Json
Foreach ($appData in $jsonContent.Apps) {
# Handle AppId as array (could be single or multiple IDs)
$appIdArray = if ($appData.AppId -is [array]) { $appData.AppId } else { @($appData.AppId) }
$appIdArray = $appIdArray | ForEach-Object { $_.Trim() } | Where-Object { $_.length -gt 0 }
$selectedByDefault = $appData.SelectedByDefault
if ($selectedByDefault -and $appIdArray.Count -gt 0) {
$appsList += $appIdArray
}
}
}
else {
# Legacy text file format
Foreach ($app in (Get-Content -Path $appsFilePath | Where-Object { $_ -notmatch '^#.*' -and $_ -notmatch '^\s*$' } )) {
if (-not ($app.IndexOf('#') -eq -1)) {
$app = $app.Substring(0, $app.IndexOf('#'))
}
$app = $app.Trim()
$appString = $app.Trim('*')
$appsList += $appString
}
}
return $appsList
}
catch {
Write-Error "Unable to read apps list from file: $appsFilePath"
AwaitKeyToExit
}
}
+31
View File
@@ -0,0 +1,31 @@
<#
.SYNOPSIS
Saves active feature settings, excluding control parameters, unless running in WhatIf mode.
#>
function Save-Settings {
if ($script:Params.ContainsKey("WhatIf")) {
Write-Host "[WhatIf] Save settings to LastUsedSettings.json" -ForegroundColor Cyan
return
}
$settings = @{
"Version" = "1.0"
"Settings" = @()
}
foreach ($param in $script:Params.Keys) {
if ($script:ControlParams -notcontains $param -and $script:Features.ContainsKey($param)) {
$value = $script:Params[$param]
$settings.Settings += @{
"Name" = $param
"Value" = $value
}
}
}
if (-not (Save-ToFile -Config $settings -FilePath $script:SavedSettingsFilePath)) {
Write-Output ""
Write-Host "Error: Failed to save settings to LastUsedSettings.json file" -ForegroundColor Red
}
}
+37
View File
@@ -0,0 +1,37 @@
<#
.SYNOPSIS
Serializes a configuration hashtable to a UTF-8 JSON file.
.PARAMETER Config
The configuration data to serialize.
.PARAMETER FilePath
The destination file path.
.PARAMETER MaxDepth
The maximum object depth passed to ConvertTo-Json.
.OUTPUTS
System.Boolean. $true when the file is written; otherwise $false.
#>
function Save-ToFile {
param (
[Parameter(Mandatory=$true)]
[hashtable]$Config,
[Parameter(Mandatory=$true)]
[string]$FilePath,
[Parameter(Mandatory=$false)]
[int]$MaxDepth = 10
)
try {
$Config | ConvertTo-Json -Depth $MaxDepth | Set-Content -Path $FilePath -Encoding UTF8 -ErrorAction Stop
return $true
}
catch {
Write-Error "Failed to write '$FilePath': $($_.Exception.Message)"
return $false
}
}
@@ -1,15 +0,0 @@
# Saves the provided appsList to the CustomAppsList file
function SaveCustomAppsListToFile {
param (
$appsList
)
$script:SelectedApps = $appsList
# Create file that stores selected apps if it doesn't exist
if (-not (Test-Path $script:CustomAppsListFilePath)) {
$null = New-Item $script:CustomAppsListFilePath -ItemType File
}
Set-Content -Path $script:CustomAppsListFilePath -Value $script:SelectedApps
}
-23
View File
@@ -1,23 +0,0 @@
# Saves the current settings, excluding control parameters, to 'LastUsedSettings.json' file
function SaveSettings {
$settings = @{
"Version" = "1.0"
"Settings" = @()
}
foreach ($param in $script:Params.Keys) {
if ($script:ControlParams -notcontains $param) {
$value = $script:Params[$param]
$settings.Settings += @{
"Name" = $param
"Value" = $value
}
}
}
if (-not (SaveToFile -Config $settings -FilePath $script:SavedSettingsFilePath)) {
Write-Output ""
Write-Host "Error: Failed to save settings to LastUsedSettings.json file" -ForegroundColor Red
}
}
-22
View File
@@ -1,22 +0,0 @@
# Saves configuration JSON to a file.
# Returns $true on success, $false on failure.
function SaveToFile {
param (
[Parameter(Mandatory=$true)]
[hashtable]$Config,
[Parameter(Mandatory=$true)]
[string]$FilePath,
[Parameter(Mandatory=$false)]
[int]$MaxDepth = 10
)
try {
$Config | ConvertTo-Json -Depth $MaxDepth | Set-Content -Path $FilePath -Encoding UTF8
return $true
}
catch {
return $false
}
}
@@ -1,6 +1,20 @@
# Applies settings from a JSON object to UI controls (checkboxes and comboboxes)
# Used by LoadDefaultsBtn and LoadLastUsedBtn in the UI
function ApplySettingsToUiControls {
<#
.SYNOPSIS
Applies enabled settings from JSON to mapped checkbox and combo-box controls.
.PARAMETER Window
The window that owns the mapped controls.
.PARAMETER SettingsJson
The settings object containing a Settings collection.
.PARAMETER UiControlMappings
The feature-to-control mapping used to locate and update controls.
.OUTPUTS
System.Boolean. $false for invalid settings input; otherwise $true.
#>
function Apply-SettingsToUiControls {
param (
$window,
$settingsJson,
@@ -1,10 +1,20 @@
# Attaches shift-click selection behavior to a checkbox in an apps panel
# Parameters:
# - $checkbox: The checkbox to attach the behavior to
# - $appsPanel: The StackPanel containing checkbox items
# - $lastSelectedCheckboxRef: A reference to a variable storing the last clicked checkbox
# - $updateStatusCallback: Optional callback to update selection status
function AttachShiftClickBehavior {
<#
.SYNOPSIS
Attaches shift-click range-selection behavior to an application checkbox.
.PARAMETER Checkbox
The checkbox that receives the mouse event handler.
.PARAMETER AppsPanel
The panel whose visible checkboxes participate in range selection.
.PARAMETER LastSelectedCheckboxRef
A reference that stores the previously clicked checkbox.
.PARAMETER UpdateStatusCallback
An optional callback invoked after a range selection changes.
#>
function Attach-ShiftClickBehavior {
param (
[System.Windows.Controls.CheckBox]$checkbox,
[System.Windows.Controls.StackPanel]$appsPanel,
@@ -1,5 +1,11 @@
# Checks if the system is set to use dark mode for apps
function GetSystemUsesDarkMode {
<#
.SYNOPSIS
Returns whether Windows apps are configured to use dark mode.
.OUTPUTS
System.Boolean. $false when the personalization setting cannot be read.
#>
function Get-SystemUsesDarkMode {
try {
$personalizeKey = Get-ItemProperty -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize'
+118 -28
View File
@@ -174,6 +174,10 @@ function Update-AppSelectionStatus {
}
}
<#
.SYNOPSIS
Updates the app-removal scope description to match the selected ComboBoxItem.
#>
function Update-AppRemovalScopeDescription {
param(
[System.Windows.Controls.ComboBox]$AppRemovalScopeCombo,
@@ -182,20 +186,60 @@ function Update-AppRemovalScopeDescription {
$selectedItem = $AppRemovalScopeCombo.SelectedItem
if ($selectedItem) {
switch ($selectedItem.Content) {
"All users" {
# Content is the display text and will change once translated; Name is stable.
switch ($selectedItem.Name) {
"AppRemovalScopeAllUsers" {
$AppRemovalScopeDescription.Text = "Apps will be removed for all users and from the Windows image to prevent reinstallation for new users."
}
"Current user only" {
$AppRemovalScopeDescription.Text = "Apps will only be removed for the current user. Existing and new users will not be affected."
"AppRemovalScopeCurrentUser" {
$AppRemovalScopeDescription.Text = "Apps will only be removed for the current user."
}
"Target user only" {
$AppRemovalScopeDescription.Text = "Apps will only be removed for the specified target user. Existing and new users will not be affected."
"AppRemovalScopeTargetUser" {
$AppRemovalScopeDescription.Text = "Apps will only be removed for the specified target user."
}
}
}
}
<#
.SYNOPSIS
Tests whether the app-removal scope combo is currently set to "Target user only".
#>
function Test-AppRemovalScopeTargetsOtherUser {
param(
[System.Windows.Controls.ComboBox]$AppRemovalScopeCombo
)
return ($AppRemovalScopeCombo -and $AppRemovalScopeCombo.SelectedItem -and $AppRemovalScopeCombo.SelectedItem.Name -eq 'AppRemovalScopeTargetUser')
}
<#
.SYNOPSIS
Resolves the -AppRemovalTarget value for the selected app-removal scope.
#>
function Get-AppRemovalScopeTarget {
param(
[System.Windows.Controls.ComboBox]$AppRemovalScopeCombo,
[System.Windows.Controls.TextBox]$OtherUsernameTextBox
)
$selectedItem = $AppRemovalScopeCombo.SelectedItem
if (-not $selectedItem) { return $null }
if (Test-AppRemovalScopeTargetsOtherUser -AppRemovalScopeCombo $AppRemovalScopeCombo) {
return $OtherUsernameTextBox.Text.Trim()
}
switch ($selectedItem.Name) {
"AppRemovalScopeAllUsers" { return 'AllUsers' }
"AppRemovalScopeCurrentUser" { return 'CurrentUser' }
default {
Write-Warning "Unrecognized app-removal scope item '$($selectedItem.Name)'. Skipping app removal."
return $null
}
}
}
function Invoke-AppPreset {
param(
[System.Windows.Controls.Panel]$AppsPanel,
@@ -223,7 +267,7 @@ function Update-AppPresetStates {
$script:UpdatingPresets = $true
try {
# Helper: count matching and checked apps, set checkbox state
function SetPresetState($CheckBox, [scriptblock]$MatchFilter) {
function Set-PresetState($CheckBox, [scriptblock]$MatchFilter) {
$total = 0; $checked = 0
foreach ($child in $AppsPanel.Children) {
if ($child -is [System.Windows.Controls.CheckBox]) {
@@ -241,15 +285,15 @@ function Update-AppPresetStates {
$presetDefaultApps = $window.FindName('PresetDefaultApps')
$presetLastUsed = $window.FindName('PresetLastUsed')
SetPresetState $presetDefaultApps { param($c) $c.SelectedByDefault -eq $true }
Set-PresetState $presetDefaultApps { param($c) $c.SelectedByDefault -eq $true }
foreach ($jsonCb in $script:JsonPresetCheckboxes) {
$localIds = $jsonCb.PresetAppIds
SetPresetState $jsonCb { param($c) (@($c.AppIds) | Where-Object { $localIds -contains $_ }).Count -gt 0 }.GetNewClosure()
Set-PresetState $jsonCb { param($c) (@($c.AppIds) | Where-Object { $localIds -contains $_ }).Count -gt 0 }.GetNewClosure()
}
# Last used preset: only update if it's visible (has saved apps)
if ($presetLastUsed.Visibility -ne 'Collapsed' -and $script:SavedAppIds) {
SetPresetState $presetLastUsed { param($c) (@($c.AppIds) | Where-Object { $script:SavedAppIds -contains $_ }).Count -gt 0 }
Set-PresetState $presetLastUsed { param($c) (@($c.AppIds) | Where-Object { $script:SavedAppIds -contains $_ }).Count -gt 0 }
}
}
finally {
@@ -304,19 +348,42 @@ function Find-ParentScrollViewer {
return $null
}
function Load-AppsWithList {
<#
.SYNOPSIS
Loads application details and adds their interactive checkboxes to the main window.
.PARAMETER Window
The main application window and resource owner.
.PARAMETER AppsPanel
The panel populated with application checkboxes.
.PARAMETER OnlyInstalledAppsBox
The filter control that determines whether only installed apps are loaded.
.PARAMETER LoadingAppsIndicator
The loading indicator shown while application details are prepared.
.PARAMETER ImportConfigBtn
The optional import control re-enabled after loading completes.
.PARAMETER ListOfApps
An optional pre-fetched list of installed WinGet applications.
#>
function Add-AppsToMainWindow {
param(
[System.Windows.Window]$Window,
[System.Windows.Controls.Panel]$AppsPanel,
[System.Windows.Controls.CheckBox]$OnlyInstalledAppsBox,
[System.Windows.Controls.Border]$LoadingAppsIndicator,
[System.Windows.Controls.MenuItem]$ImportConfigBtn,
[string]$ListOfApps
[object[]]$ListOfApps
)
$script:MainWindowLastSelectedCheckbox = $null
$loaderScriptPath = $script:LoadAppsDetailsScriptPath
$helperScriptPath = $script:TestAppInWingetListScriptPath
$appsFilePath = $script:AppsListFilePath
$onlyInstalled = [bool]$OnlyInstalledAppsBox.IsChecked
@@ -326,13 +393,16 @@ function Load-AppsWithList {
$script:PreloadedAppData = $null
}
else {
# Load apps details in a background job to keep the UI responsive
# Load apps details in a background job to keep the UI responsive.
# The helper is dot-sourced inside the job because the runspace
# does not inherit the parent scope's dot-sourced functions.
$rawAppData = Invoke-NonBlocking -ScriptBlock {
param($loaderScript, $appsListFilePath, $installedList, $onlyInstalled)
param($loaderScript, $helperScript, $appsListFilePath, $installedList, $onlyInstalled)
$script:AppsListFilePath = $appsListFilePath
. $helperScript
. $loaderScript
LoadAppsDetailsFromJson -OnlyInstalled:$onlyInstalled -InstalledList $installedList -InitialCheckedFromJson:$false
} -ArgumentList $loaderScriptPath, $appsFilePath, $ListOfApps, $onlyInstalled
Import-AppDetailsFromJson -OnlyInstalled:$onlyInstalled -InstalledList $installedList -InitialCheckedFromJson:$false
} -ArgumentList $loaderScriptPath, $helperScriptPath, $appsFilePath, $ListOfApps, $onlyInstalled
}
$appsToAdd = @($rawAppData | Where-Object { $_ -and ($_.AppId -or $_.FriendlyName) } | Sort-Object -Property FriendlyName)
@@ -348,10 +418,9 @@ function Load-AppsWithList {
return
}
$brushSafe = [System.Windows.Media.BrushConverter]::new().ConvertFromString('#4CAF50')
$brushUnsafe = [System.Windows.Media.BrushConverter]::new().ConvertFromString('#F44336')
$brushDefault = [System.Windows.Media.BrushConverter]::new().ConvertFromString('#FFC107')
$brushSafe.Freeze(); $brushUnsafe.Freeze(); $brushDefault.Freeze()
$brushSafe = $Window.Resources['AppRecommendationSafeColor']
$brushDefault = $Window.Resources['AppRecommendationOptionalColor']
$brushUnsafe = $Window.Resources['AppRecommendationUnsafeColor']
# Create WPF controls; pump the Dispatcher every batch so the spinner keeps animating.
$batchSize = 20
@@ -381,7 +450,7 @@ function Load-AppsWithList {
$dot.ToolTip = switch ($app.Recommendation) {
'safe' { '[Recommended] Safe to remove for most users' }
'unsafe' { '[Not Recommended] Only remove if you know what you are doing' }
default { "[Optional] Remove if you don't need this app" }
default { "[Optional] Can be safely removed if you don't need this app" }
}
[System.Windows.Controls.Grid]::SetColumn($dot, 0)
@@ -432,7 +501,7 @@ function Load-AppsWithList {
-AppRemovalScopeDescription $w.FindName('AppRemovalScopeDescription') `
-UserSelectionCombo $w.FindName('UserSelectionCombo')
})
AttachShiftClickBehavior -checkbox $checkbox -appsPanel $AppsPanel `
Attach-ShiftClickBehavior -checkbox $checkbox -appsPanel $AppsPanel `
-lastSelectedCheckboxRef ([ref]$script:MainWindowLastSelectedCheckbox) `
-updateStatusCallback {
$w = $script:MainWindow
@@ -446,7 +515,7 @@ function Load-AppsWithList {
$AppsPanel.Children.Add($checkbox) | Out-Null
if (($i + 1) % $batchSize -eq 0) { DoEvents }
if (($i + 1) % $batchSize -eq 0) { Invoke-DoEvents }
}
$sortArrowName = $Window.FindName('SortArrowName')
@@ -477,7 +546,26 @@ function Load-AppsWithList {
}
}
function Load-AppsIntoMainUI {
<#
.SYNOPSIS
Starts asynchronous loading of application checkboxes for the main window.
.PARAMETER Window
The main application window.
.PARAMETER AppsPanel
The panel that receives application checkboxes.
.PARAMETER OnlyInstalledAppsBox
The installed-applications filter control.
.PARAMETER LoadingAppsIndicator
The loading indicator shown until loading completes.
.PARAMETER ImportConfigBtn
The optional import control disabled while loading is in progress.
#>
function Initialize-MainWindowApps {
param(
[System.Windows.Window]$Window,
[System.Windows.Controls.Panel]$AppsPanel,
@@ -508,22 +596,24 @@ function Load-AppsIntoMainUI {
# Force a render so the loading indicator is visible, then schedule the
# actual loading at Background priority so this call returns immediately.
# This is critical when called from Add_Loaded: the window must finish
# its initialization before we start a nested message pump via DoEvents.
# its initialization before we start a nested message pump via Invoke-DoEvents.
$Window.Dispatcher.Invoke([System.Windows.Threading.DispatcherPriority]::Render, [action] {})
$Window.Dispatcher.BeginInvoke([System.Windows.Threading.DispatcherPriority]::Background, [action] {
try {
$listOfApps = ""
$listOfApps = $null
if ($OnlyInstalledAppsBox.IsChecked -and ($script:WingetInstalled -eq $true)) {
$listOfApps = GetInstalledAppsViaWinget -TimeOut 10 -NonBlocking
Write-Host "Retrieving installed apps via winget..."
$listOfApps = Get-WingetInstalledApps -TimeOut 20 -NonBlocking
if ($null -eq $listOfApps) {
Write-Warning "WinGet returned no data (command timed out or failed)"
Show-MessageBox -Message 'Unable to load list of installed apps via WinGet.' -Title 'Error' -Button 'OK' -Icon 'Error' | Out-Null
$OnlyInstalledAppsBox.IsChecked = $false
}
}
Load-AppsWithList -Window $Window -AppsPanel $AppsPanel -OnlyInstalledAppsBox $OnlyInstalledAppsBox `
Add-AppsToMainWindow -Window $Window -AppsPanel $AppsPanel -OnlyInstalledAppsBox $OnlyInstalledAppsBox `
-LoadingAppsIndicator $LoadingAppsIndicator -ImportConfigBtn $ImportConfigBtn -ListOfApps $listOfApps
}
catch {
+57 -40
View File
@@ -1,29 +1,8 @@
# MainWindow-Deployment.ps1
# Overview generation, pending tweak actions, feature labels, tweak preset maps, apply logic, user mode state, user selection, and validation.
function Get-FeatureLabel {
param(
[string]$FeatureId,
$FallbackLabel = $null
)
$label = $script:FeatureLabelLookup[$FeatureId]
if (-not [string]::IsNullOrWhiteSpace([string]$label)) {
return [string]$label
}
if (-not [string]::IsNullOrWhiteSpace([string]$FallbackLabel)) {
return [string]$FallbackLabel
}
return [string]$FeatureId
}
function Get-UndoFeatureLabel {
param(
[string]$FeatureId,
$FallbackLabel = $null
)
param([string]$FeatureId)
$undoLabel = $script:UndoFeatureLabelLookup[$FeatureId]
if (-not [string]::IsNullOrWhiteSpace([string]$undoLabel)) {
@@ -31,10 +10,16 @@ function Get-UndoFeatureLabel {
}
# Fall back to the regular label (prefixed for undo context)
$label = Get-FeatureLabel -FeatureId $FeatureId -FallbackLabel $FallbackLabel
return [string]$label
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,
@@ -69,14 +54,14 @@ function Get-PendingTweakActions {
$actions.Add([PSCustomObject]@{
Action = 'Apply'
FeatureId = [string]$mapping.FeatureId
Label = (Get-FeatureLabel -FeatureId $mapping.FeatureId -FallbackLabel $mapping.Label)
Label = [string]$script:FeatureLabelLookup[$mapping.FeatureId]
})
}
elseif ($wasApplied -and -not $isNowChecked) {
$actions.Add([PSCustomObject]@{
Action = 'Undo'
FeatureId = [string]$mapping.FeatureId
Label = (Get-FeatureLabel -FeatureId $mapping.FeatureId -FallbackLabel $mapping.Label)
Label = [string](Get-UndoFeatureLabel -FeatureId $mapping.FeatureId)
})
}
}
@@ -101,7 +86,7 @@ function Get-PendingTweakActions {
$actions.Add([PSCustomObject]@{
Action = 'Apply'
FeatureId = [string]$fid
Label = (Get-FeatureLabel -FeatureId $fid)
Label = [string]$script:FeatureLabelLookup[$fid]
})
}
}
@@ -162,7 +147,20 @@ function Invoke-ShowChangesOverview {
Show-MessageBox -Message $message -Title 'Selected Changes' -Button 'OK' -Icon 'None' -Width 600
}
function Build-TweakPresetControlMap {
<#
.SYNOPSIS
Builds the control values needed to apply a saved tweak preset.
.PARAMETER Window
The window that owns the visible tweak controls.
.PARAMETER SettingsJson
The saved settings object to translate into control values.
.OUTPUTS
System.Collections.Hashtable. Control metadata keyed by control name.
#>
function Get-TweakPresetControlMap {
param(
[System.Windows.Window]$Window,
$SettingsJson
@@ -173,7 +171,7 @@ function Build-TweakPresetControlMap {
return $presetMap
}
# FeatureId -> control metadata, similar to ApplySettingsToUiControls lookup.
# FeatureId -> control metadata, similar to Apply-SettingsToUiControls lookup.
$featureIdIndex = @{}
foreach ($controlName in $script:UiControlMappings.Keys) {
$control = $Window.FindName($controlName)
@@ -214,10 +212,23 @@ function Build-TweakPresetControlMap {
return $presetMap
}
function Build-CategoryTweakPresetMap {
<#
.SYNOPSIS
Builds the enabled state map for visible tweak controls in a category.
.PARAMETER Window
The window that owns the visible tweak controls.
.PARAMETER CategoryId
The stable CategoryId (from Features.json) whose mapped controls are included.
.OUTPUTS
System.Collections.Hashtable. Control metadata keyed by control name.
#>
function Get-CategoryTweakPresetMap {
param(
[System.Windows.Window]$Window,
[string]$Category
[string]$CategoryId
)
$presetMap = @{}
@@ -225,7 +236,7 @@ function Build-CategoryTweakPresetMap {
foreach ($controlName in $script:UiControlMappings.Keys) {
$mapping = $script:UiControlMappings[$controlName]
if ($mapping.Category -ne $Category) { continue }
if ($mapping.CategoryId -ne $CategoryId) { continue }
$control = $Window.FindName($controlName)
if (-not $control -or $control.Visibility -ne 'Visible') { continue }
@@ -390,10 +401,10 @@ function Initialize-TweakPresetSources {
$LastUsedSettingsJson
)
$script:DefaultTweakPresetMap = Build-TweakPresetControlMap -Window $Window -SettingsJson $DefaultSettingsJson
$script:LastUsedTweakPresetMap = Build-TweakPresetControlMap -Window $Window -SettingsJson $LastUsedSettingsJson
$script:PrivacyTweakPresetMap = Build-CategoryTweakPresetMap -Window $Window -Category 'Privacy & Suggested Content'
$script:AITweakPresetMap = Build-CategoryTweakPresetMap -Window $Window -Category 'AI'
$script:DefaultTweakPresetMap = Get-TweakPresetControlMap -Window $Window -SettingsJson $DefaultSettingsJson
$script:LastUsedTweakPresetMap = Get-TweakPresetControlMap -Window $Window -SettingsJson $LastUsedSettingsJson
$script:PrivacyTweakPresetMap = Get-CategoryTweakPresetMap -Window $Window -CategoryId 'PrivacySuggestedContent'
$script:AITweakPresetMap = Get-CategoryTweakPresetMap -Window $Window -CategoryId 'AI'
$presetLastUsedTweaksBtn = $Window.FindName('PresetLastUsedTweaksBtn')
if ($presetLastUsedTweaksBtn) {
@@ -436,7 +447,7 @@ function Update-UserSelectionDescription {
switch ($UserSelectionCombo.SelectedIndex) {
0 {
$currentUserName = GetUserName
$currentUserName = Get-UserName
if ([string]::IsNullOrWhiteSpace($currentUserName)) {
$UserSelectionDescription.Text = "The currently logged-in user profile"
}
@@ -457,6 +468,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 {
@@ -464,11 +478,14 @@ function Test-OtherUsername {
[System.Windows.Window]$Window,
[System.Windows.Controls.ComboBox]$UserSelectionCombo,
[System.Windows.Controls.TextBox]$OtherUsernameTextBox,
[System.Windows.Controls.TextBlock]$UsernameValidationMessage
[System.Windows.Controls.TextBlock]$UsernameValidationMessage,
[System.Windows.Controls.ComboBox]$AppRemovalScopeCombo
)
# Only validate if "Other User" is selected
if ($UserSelectionCombo.SelectedIndex -ne 1) {
# Only validate if "Other User" is the deployment target, or "Target user only" is the app-removal scope
$isOtherUserSelected = ($UserSelectionCombo.SelectedIndex -eq 1)
$isAppRemovalTargetUserSelected = Test-AppRemovalScopeTargetsOtherUser -AppRemovalScopeCombo $AppRemovalScopeCombo
if (-not $isOtherUserSelected -and -not $isAppRemovalTargetUserSelected) {
return $true
}
+153 -20
View File
@@ -1,13 +1,26 @@
# MainWindow-TweaksBuilder.ps1
# Dynamic tweaks UI construction from Features.json, tweak state management, selection clear, and search/highlight.
function Build-DynamicTweaks {
<#
.SYNOPSIS
Builds the main window's dynamic tweak controls from Features.json.
.PARAMETER Window
The main window whose category columns receive the generated controls.
.PARAMETER WinVersion
The Windows build number used for the category-icon fallback.
.NOTES
Initializes script-scoped control and category mappings used by the tweak UI.
#>
function New-DynamicTweakControls {
param(
[System.Windows.Window]$Window,
[int]$WinVersion
)
$featuresJson = LoadJsonFile -filePath $script:FeaturesFilePath -expectedVersion "1.0"
$featuresJson = Import-JsonFile -filePath $script:FeaturesFilePath -expectedVersion "1.0"
if (-not $featuresJson) {
throw "Unable to load Features.json file. The GUI cannot continue without feature definitions."
@@ -29,7 +42,26 @@ function Build-DynamicTweaks {
$script:TweaksCompactMode = $null
$script:TweaksCardsMovedFromCol2 = @()
function CreateLabeledCombo($parent, $labelText, $comboName, $items) {
<#
.SYNOPSIS
Creates and registers a labeled combo box or a checkbox for a tweak.
.PARAMETER Parent
The panel that receives the generated control.
.PARAMETER LabelText
The display and automation label for the tweak.
.PARAMETER ComboName
The name used to register the generated control.
.PARAMETER Items
The available tweak options; two options produce a checkbox.
.OUTPUTS
System.Windows.Controls.Control. The generated checkbox or combo box.
#>
function New-LabeledCombo($parent, $labelText, $comboName, $items) {
# If only 2 items (No Change + one option), use a checkbox instead
if ($items.Count -eq 2) {
$checkbox = New-Object System.Windows.Controls.CheckBox
@@ -96,7 +128,17 @@ function Build-DynamicTweaks {
return $combo
}
function GetWikiUrlForCategory($category) {
<#
.SYNOPSIS
Returns the Features wiki URL for a tweak category.
.PARAMETER Category
The category name converted to a wiki anchor.
.OUTPUTS
System.String. The category URL, or the Features page for an empty category.
#>
function Get-WikiUrlForCategory($category) {
if (-not $category) { return 'https://github.com/Raphire/Win11Debloat/wiki/Features' }
$slug = $category.ToLowerInvariant()
@@ -107,7 +149,17 @@ function Build-DynamicTweaks {
return "https://github.com/Raphire/Win11Debloat/wiki/Features#$slug"
}
function GetOrCreateCategoryCard($categoryObj) {
<#
.SYNOPSIS
Returns the existing category panel or creates and registers a new one.
.PARAMETER CategoryObj
The category definition containing Name and Icon properties.
.OUTPUTS
System.Windows.Controls.StackPanel. The category's content panel.
#>
function Get-OrCreateCategoryCard($categoryObj) {
$categoryName = $categoryObj.Name
$categoryIcon = $categoryObj.Icon
@@ -132,6 +184,9 @@ function Build-DynamicTweaks {
# Convert HTML entity to character (e.g., &#xE72E; -> 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']
@@ -148,8 +203,8 @@ function Build-DynamicTweaks {
$helpBtn = New-Object System.Windows.Controls.Button
$helpBtn.Content = $helpIcon
$helpBtn.ToolTip = "Open wiki for more info on '$categoryName' tweaks"
$helpBtn.Tag = (GetWikiUrlForCategory -category $categoryName)
$helpBtn.ToolTip = "Open the wiki for more info on '$categoryName' tweaks"
$helpBtn.Tag = (Get-WikiUrlForCategory -category $categoryName)
$helpBtn.Style = $Window.Resources['CategoryHelpLinkButtonStyle']
$helpBtn.Add_Click({
param($button, $e)
@@ -179,8 +234,17 @@ function Build-DynamicTweaks {
foreach ($c in $featuresJson.Categories) {
$categoryName = if ($c -is [string]) { $c } else { $c.Name }
if ($categoriesPresent.ContainsKey($categoryName)) {
# Store the full category object (or create one with default icon for string categories)
$categoryObj = if ($c -is [string]) { @{Name = $c; Icon = '&#xE712;' } } else { $c }
# Store the full category object (or create one with default icon for string categories).
# A category without its own CategoryId falls back to its Name, same as before CategoryId existed.
$categoryObj = if ($c -is [string]) {
@{Name = $c; CategoryId = $c; Icon = '&#xE712;' }
}
elseif (-not $c.CategoryId) {
@{Name = $c.Name; CategoryId = $c.Name; Icon = $c.Icon }
}
else {
$c
}
$orderedCategories += $categoryObj
}
}
@@ -188,16 +252,22 @@ function Build-DynamicTweaks {
else {
# For backward compatibility, create category objects from keys
foreach ($catName in $categoriesPresent.Keys) {
$orderedCategories += @{Name = $catName; Icon = '&#xE712;' }
$orderedCategories += @{Name = $catName; CategoryId = $catName; Icon = '&#xE712;' }
}
}
# Build a FeatureId -> feature lookup for version filtering in groups
$featureMap = @{}
foreach ($f in $featuresJson.Features) {
$featureMap[$f.FeatureId] = $f
}
foreach ($categoryObj in $orderedCategories) {
$categoryName = $categoryObj.Name
$categoryId = $categoryObj.CategoryId
# 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 = @()
@@ -250,9 +320,64 @@ function Build-DynamicTweaks {
foreach ($item in $sortedItems) {
if ($item.Type -eq 'group') {
$group = $item.Data
$items = @('No Change') + ($group.Values | ForEach-Object { $_.Label })
# Filter values by Windows version compatibility of their referenced features
$filteredValues = @($group.Values | Where-Object {
$allCompatible = $true
foreach ($fid in $_.FeatureIds) {
if ($featureMap.ContainsKey($fid)) {
$f = $featureMap[$fid]
if (($f.MinVersion -and $WinVersion -lt $f.MinVersion) -or ($f.MaxVersion -and $WinVersion -gt $f.MaxVersion)) {
$allCompatible = $false
break
}
}
}
$allCompatible
})
# Skip the group entirely if all values are incompatible with this Windows version
if ($filteredValues.Count -eq 0) { continue }
# When only 1 value remains, render as the underlying feature directly
if ($filteredValues.Count -eq 1) {
$featureIds = $filteredValues[0].FeatureIds
if (-not $featureIds -or $featureIds.Count -eq 0) { continue }
$soleFid = $featureIds[0]
if ($featureMap.ContainsKey($soleFid)) {
$soleFeature = $featureMap[$soleFid]
$opt = 'Apply'
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 = Get-OrCreateCategoryCard -categoryObj $categoryObj }
$combo = New-LabeledCombo -parent $panel -labelText $soleFeature.Label -comboName $comboName -items $items
# attach tooltip from Features.json if present
if ($soleFeature.ToolTip -or $soleFeature.DisableWhenApplied -eq $true) {
$tooltipText = $soleFeature.ToolTip
if ($soleFeature.DisableWhenApplied -eq $true) {
$tooltipText = "This tweak is already applied and cannot be undone automatically. Visit the Win11Debloat wiki for instructions on how to manually revert this change."
}
$tipBlock = New-Object System.Windows.Controls.TextBlock
$tipBlock.Text = $tooltipText
$tipBlock.TextWrapping = 'Wrap'
$tipBlock.MaxWidth = 420
$combo.ToolTip = $tipBlock
[System.Windows.Controls.ToolTipService]::SetShowOnDisabled($combo, $true)
$lblBorderObj = $null
try { $lblBorderObj = $Window.FindName("$comboName`_LabelBorder") } catch {}
if ($lblBorderObj) { $lblBorderObj.ToolTip = $tipBlock }
}
$script:UiControlMappings[$comboName] = @{ Type = 'feature'; FeatureId = $soleFeature.FeatureId; Label = $soleFeature.Label; CategoryId = $categoryId }
}
continue
}
$items = @('No Change') + ($filteredValues | ForEach-Object { $_.Label })
$comboName = 'Group_{0}Combo' -f $group.GroupId
$combo = CreateLabeledCombo -parent $panel -labelText $group.Label -comboName $comboName -items $items
if (-not $panel) { $panel = Get-OrCreateCategoryCard -categoryObj $categoryObj }
$combo = New-LabeledCombo -parent $panel -labelText $group.Label -comboName $comboName -items $items
# attach tooltip from UiGroups if present
if ($group.ToolTip) {
$tipBlock = New-Object System.Windows.Controls.TextBlock
@@ -264,7 +389,7 @@ function Build-DynamicTweaks {
try { $lblBorderObj = $Window.FindName("$comboName`_LabelBorder") } catch {}
if ($lblBorderObj) { $lblBorderObj.ToolTip = $tipBlock }
}
$script:UiControlMappings[$comboName] = @{ Type = 'group'; Values = $group.Values; Label = $group.Label; Category = $categoryName }
$script:UiControlMappings[$comboName] = @{ Type = 'group'; Values = $filteredValues; Label = $group.Label; CategoryId = $categoryId }
}
elseif ($item.Type -eq 'feature') {
$feature = $item.Data
@@ -272,7 +397,8 @@ function Build-DynamicTweaks {
if ($feature.FeatureId -match '^Disable') { $opt = 'Disable' } elseif ($feature.FeatureId -match '^Enable') { $opt = 'Enable' }
$items = @('No Change', $opt)
$comboName = ("Feature_{0}_Combo" -f $feature.FeatureId) -replace '[^a-zA-Z0-9_]', ''
$combo = CreateLabeledCombo -parent $panel -labelText $feature.Label -comboName $comboName -items $items
if (-not $panel) { $panel = Get-OrCreateCategoryCard -categoryObj $categoryObj }
$combo = New-LabeledCombo -parent $panel -labelText $feature.Label -comboName $comboName -items $items
# attach tooltip from Features.json if present, and include the disabled-state reason
if ($feature.ToolTip -or $feature.DisableWhenApplied -eq $true) {
$tooltipText = $feature.ToolTip
@@ -290,7 +416,7 @@ function Build-DynamicTweaks {
try { $lblBorderObj = $Window.FindName("$comboName`_LabelBorder") } catch {}
if ($lblBorderObj) { $lblBorderObj.ToolTip = $tipBlock }
}
$script:UiControlMappings[$comboName] = @{ Type = 'feature'; FeatureId = $feature.FeatureId; Label = $feature.Label; Category = $categoryName }
$script:UiControlMappings[$comboName] = @{ Type = 'feature'; FeatureId = $feature.FeatureId; Label = $feature.Label; CategoryId = $categoryId }
}
}
}
@@ -313,7 +439,7 @@ function Update-CurrentTweakSystemState {
if (-not $script:UiControlMappings) { return }
if (-not $script:Features) { return }
$featuresJson = LoadJsonFile -filePath $script:FeaturesFilePath -expectedVersion "1.0"
$featuresJson = Import-JsonFile -filePath $script:FeaturesFilePath -expectedVersion "1.0"
if (-not $featuresJson) { return }
$groupMap = @{}
@@ -359,7 +485,14 @@ function Update-CurrentTweakSystemState {
}
}
function Load-CurrentTweakStateIntoUI {
<#
.SYNOPSIS
Updates tweak controls to reflect the current system state.
.PARAMETER Window
The window that owns the generated tweak controls.
#>
function Set-CurrentTweakStateInUi {
param([System.Windows.Window]$Window)
Update-CurrentTweakSystemState -Window $Window -ApplyToUi:$true
+24 -72
View File
@@ -1,71 +1,5 @@
# MainWindow-WindowChrome.ps1
# Window sizing, DPI-aware coordinate conversion, maximized-window taskbar-constraint helpers, and UI animations.
function Register-MaximizedWindowHelper {
if (-not ([System.Management.Automation.PSTypeName]'Win11Debloat.MaximizedWindowHelper').Type) {
Add-Type -Namespace Win11Debloat -Name MaximizedWindowHelper `
-ReferencedAssemblies 'PresentationFramework','System.Windows.Forms','System.Drawing' `
-MemberDefinition @'
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
private struct MINMAXINFO {
public POINT ptReserved, ptMaxSize, ptMaxPosition, ptMinTrackSize, ptMaxTrackSize;
}
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
private struct POINT { public int x, y; }
[System.Runtime.InteropServices.DllImport("user32.dll")]
private static extern System.IntPtr MonitorFromWindow(System.IntPtr hwnd, uint dwFlags);
[System.Runtime.InteropServices.DllImport("user32.dll", CharSet = System.Runtime.InteropServices.CharSet.Auto)]
private static extern bool GetMonitorInfo(System.IntPtr hMonitor, ref MONITORINFO lpmi);
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
private struct RECT {
public int Left, Top, Right, Bottom;
}
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential, CharSet = System.Runtime.InteropServices.CharSet.Auto)]
private struct MONITORINFO {
public int cbSize;
public RECT rcMonitor;
public RECT rcWork;
public uint dwFlags;
}
public static System.IntPtr WmGetMinMaxInfoHook(
System.IntPtr hwnd, int msg, System.IntPtr wParam, System.IntPtr lParam, ref bool handled) {
if (msg == 0x0024) { // WM_GETMINMAXINFO
var mmi = (MINMAXINFO)System.Runtime.InteropServices.Marshal.PtrToStructure(
lParam, typeof(MINMAXINFO));
const uint MONITOR_DEFAULTTONEAREST = 0x00000002;
var monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
var monitorInfo = new MONITORINFO();
monitorInfo.cbSize = System.Runtime.InteropServices.Marshal.SizeOf(typeof(MONITORINFO));
if (monitor != System.IntPtr.Zero && GetMonitorInfo(monitor, ref monitorInfo)) {
mmi.ptMaxPosition.x = monitorInfo.rcWork.Left - monitorInfo.rcMonitor.Left;
mmi.ptMaxPosition.y = monitorInfo.rcWork.Top - monitorInfo.rcMonitor.Top;
mmi.ptMaxSize.x = monitorInfo.rcWork.Right - monitorInfo.rcWork.Left;
mmi.ptMaxSize.y = monitorInfo.rcWork.Bottom - monitorInfo.rcWork.Top;
}
else {
var screen = System.Windows.Forms.Screen.FromHandle(hwnd);
var wa = screen.WorkingArea;
var bounds = screen.Bounds;
mmi.ptMaxPosition.x = wa.Left - bounds.Left;
mmi.ptMaxPosition.y = wa.Top - bounds.Top;
mmi.ptMaxSize.x = wa.Width;
mmi.ptMaxSize.y = wa.Height;
}
System.Runtime.InteropServices.Marshal.StructureToPtr(mmi, lParam, true);
}
return System.IntPtr.Zero;
}
'@
}
}
# Window sizing, DPI-aware coordinate conversion, and UI animations.
# Convert screen-pixel coordinates to WPF device-independent pixels (DIP)
function ConvertTo-ScreenPointToDip {
@@ -118,16 +52,35 @@ function Update-MainWindowChrome {
)
$windowStateMaximized = [System.Windows.WindowState]::Maximized
$chrome = [System.Windows.Shell.WindowChrome]::GetWindowChrome($Window)
if ($Window.WindowState -eq $windowStateMaximized) {
$MainBorder.Margin = [System.Windows.Thickness]::new(0)
$chrome = [System.Windows.Shell.WindowChrome]::GetWindowChrome($Window)
$resizeBorder = if ($chrome) { $chrome.ResizeBorderThickness } else { [System.Windows.SystemParameters]::WindowResizeBorderThickness }
# Compute margins using screen bounds vs working area
$marginLeft = $resizeBorder.Left
$marginTop = $resizeBorder.Top
$marginRight = $resizeBorder.Right
$marginBottom = $resizeBorder.Bottom
$screen = Get-WindowScreen -Window $Window
if ($screen) {
$workTL = ConvertTo-ScreenPointToDip -Window $Window -X $screen.WorkingArea.Left -Y $screen.WorkingArea.Top
$workSize = ConvertTo-ScreenPixelsToDip -Window $Window -Width $screen.WorkingArea.Width -Height $screen.WorkingArea.Height
$screenTL = ConvertTo-ScreenPointToDip -Window $Window -X $screen.Bounds.Left -Y $screen.Bounds.Top
$screenSize = ConvertTo-ScreenPixelsToDip -Window $Window -Width $screen.Bounds.Width -Height $screen.Bounds.Height
$marginLeft += ($workTL.X - $screenTL.X)
$marginTop += ($workTL.Y - $screenTL.Y)
$marginRight += ($screenTL.X + $screenSize.Width) - ($workTL.X + $workSize.Width)
$marginBottom += ($screenTL.Y + $screenSize.Height) - ($workTL.Y + $workSize.Height)
}
$MainBorder.Margin = [System.Windows.Thickness]::new($marginLeft, $marginTop, $marginRight, $marginBottom)
$MainBorder.BorderThickness = [System.Windows.Thickness]::new(0)
$MainBorder.CornerRadius = [System.Windows.CornerRadius]::new(0)
$MainBorder.Effect = $null
$TitleBarBackground.CornerRadius = [System.Windows.CornerRadius]::new(0)
# Zero out resize borders when maximized so the entire title bar row is draggable
if ($chrome) { $chrome.ResizeBorderThickness = [System.Windows.Thickness]::new(0) }
}
else {
$MainBorder.Margin = [System.Windows.Thickness]::new(0)
@@ -135,7 +88,6 @@ function Update-MainWindowChrome {
$MainBorder.CornerRadius = [System.Windows.CornerRadius]::new(8)
$MainBorder.Effect = $NormalWindowShadow
$TitleBarBackground.CornerRadius = [System.Windows.CornerRadius]::new(8, 8, 0, 0)
if ($chrome) { $chrome.ResizeBorderThickness = [System.Windows.Thickness]::new(5) }
}
}
@@ -1,3 +1,11 @@
<#
.SYNOPSIS
Creates a lightweight state object for the restore-backup dialog result.
.DESCRIPTION
Encapsulates the user's dialog choice (Result), the selected backup file
path, and the parsed backup payload so callers receive a single object.
#>
function New-RestoreDialogState {
param(
[string]$Result = 'Cancel',
@@ -8,6 +16,16 @@ function New-RestoreDialogState {
return @{ Result = $Result; SelectedFile = $SelectedFile; Backup = $Backup }
}
<#
.SYNOPSIS
Looks up a feature definition by ID from the provided feature catalog.
.PARAMETER FeatureId
The identifier to search for (e.g. 'DisableTelemetry').
.PARAMETER Features
A hashtable loaded from Features.json (FeatureId -> feature object).
#>
function Get-RestoreDialogFeatureDefinition {
param(
[string]$FeatureId,
@@ -25,6 +43,21 @@ function Get-RestoreDialogFeatureDefinition {
return $null
}
<#
.SYNOPSIS
Determines whether a feature can be automatically reverted via registry restore.
.DESCRIPTION
Returns $true when the feature has a non-empty RegistryKey, indicating
an apply .reg file exists that can be undone automatically. Features
with custom logic (no RegistryKey) must be manually reverted.
.PARAMETER FeatureId
The feature identifier to check.
.PARAMETER Features
A hashtable loaded from Features.json.
#>
function Test-RestoreDialogFeatureCanAutoRevert {
param(
[string]$FeatureId,
@@ -43,6 +76,20 @@ function Test-RestoreDialogFeatureCanAutoRevert {
return $false
}
<#
.SYNOPSIS
Resolves a human-readable label for a feature shown in the restore dialog.
.DESCRIPTION
Returns the feature's Label from Features.json when found, falling back
to the raw FeatureId string. For null/empty FeatureIds returns 'Unknown feature'.
.PARAMETER FeatureId
The feature identifier to resolve a label for.
.PARAMETER Features
A hashtable loaded from Features.json.
#>
function Get-RestoreDialogFeatureDisplayLabel {
param(
[string]$FeatureId,
@@ -54,13 +101,28 @@ function Get-RestoreDialogFeatureDisplayLabel {
}
$featureDefinition = Get-RestoreDialogFeatureDefinition -FeatureId $FeatureId -Features $Features
if ($featureDefinition -and -not [string]::IsNullOrWhiteSpace([string]$featureDefinition.Label)) {
if ($featureDefinition) {
return [string]$featureDefinition.Label
}
return $FeatureId
}
<#
.SYNOPSIS
Checks whether a feature should appear in the restore dialog's overview list.
.DESCRIPTION
A feature is considered visible when it exists in the catalog and has
a non-empty Category (meaning it belongs to a UI grouping). Features
without a Category are hidden from the overview.
.PARAMETER FeatureId
The feature identifier to check.
.PARAMETER Features
A hashtable loaded from Features.json.
#>
function Test-RestoreDialogFeatureVisibleInOverview {
param(
[string]$FeatureId,
@@ -79,6 +141,13 @@ function Test-RestoreDialogFeatureVisibleInOverview {
return -not [string]::IsNullOrWhiteSpace([string]$featureDefinition.Category)
}
<#
.SYNOPSIS
Extracts deduplicated forward (apply) feature IDs from a backup payload.
.PARAMETER SelectedBackup
The parsed backup object containing a SelectedFeatures property.
#>
function Get-SelectedForwardFeatureIdsFromBackup {
param($SelectedBackup)
@@ -99,6 +168,13 @@ function Get-SelectedForwardFeatureIdsFromBackup {
return @($selectedFeatureIds.ToArray())
}
<#
.SYNOPSIS
Extracts deduplicated undo feature IDs from a backup payload.
.PARAMETER SelectedBackup
The parsed backup object containing a SelectedUndoFeatures property.
#>
function Get-SelectedUndoFeatureIdsFromBackup {
param($SelectedBackup)
@@ -119,6 +195,13 @@ function Get-SelectedUndoFeatureIdsFromBackup {
return @($selectedUndoFeatureIds.ToArray())
}
<#
.SYNOPSIS
Merges forward and undo feature IDs from a backup into a single deduplicated list.
.PARAMETER SelectedBackup
The parsed backup object containing SelectedFeatures and SelectedUndoFeatures.
#>
function Get-CombinedSelectedFeatureIdsFromBackup {
param($SelectedBackup)
@@ -139,12 +222,34 @@ function Get-CombinedSelectedFeatureIdsFromBackup {
return @($featureIds.ToArray())
}
<#
.SYNOPSIS
Convenience wrapper that returns all combined feature IDs from a backup.
.PARAMETER SelectedBackup
The parsed backup object.
#>
function Get-SelectedFeatureIdsFromBackup {
param($SelectedBackup)
return @(Get-CombinedSelectedFeatureIdsFromBackup -SelectedBackup $SelectedBackup)
}
<#
.SYNOPSIS
Splits selected feature IDs into revertible and non-revertible lists for display.
.DESCRIPTION
Iterates the provided feature IDs, filters to those visible in the overview,
and separates them into auto-revertible (has a RegistryKey) and non-revertible
(requires manual undo) buckets. Each entry includes a display label.
.PARAMETER SelectedFeatureIds
The list of feature IDs to categorize.
.PARAMETER Features
A hashtable loaded from Features.json.
#>
function Get-RestoreBackupFeatureLists {
param(
[string[]]$SelectedFeatureIds,
+144
View File
@@ -0,0 +1,144 @@
<#
.SYNOPSIS
Applies light or dark theme colors to a WPF window's resource dictionary.
.DESCRIPTION
Iterates over a predefined set of theme color categories and
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. 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.
.PARAMETER usesDarkMode
When $true, dark theme colors are applied; when $false, light theme colors.
.EXAMPLE
Set-WindowThemeResources -window $MainWindow -usesDarkMode $true
.EXAMPLE
Set-WindowThemeResources -window $Dialog -usesDarkMode $false
#>
# Sets resource colors for a WPF window based on dark mode preference
function Set-WindowThemeResources {
param (
$window,
[bool]$usesDarkMode
)
$ThemeColor = @{
App = @{
AccentColor = @{ Light = '#ffae00'; Dark = '#ffd700' }
BorderColor = @{ Light = '#ededed'; Dark = '#404040' }
BgColor = @{ Light = '#f3f3f3'; Dark = '#202020' }
FgColor = @{ Light = '#000000'; Dark = '#ffffff' }
IdColor = @{ Light = '#666666'; Dark = '#afafaf' }
}
Card = @{
BgColor = @{ Light = '#fbfbfb'; Dark = '#2b2b2b' }
}
Button = @{
BorderColor = @{ Light = '#d3d3d3'; Dark = '#404040' }
BgColor = @{ Light = '#0067c0'; Dark = '#0067c0' }
DisabledColor = @{ Light = '#bfbfbf'; Dark = '#434343' }
HoverColor = @{ Light = '#1975c5'; Dark = '#1975c5' }
PressedColor = @{ Light = '#3183ca'; Dark = '#3183ca' }
TextDisabledColor = @{ Light = '#ffffff'; Dark = '#989898' }
}
SecondaryButton = @{
BgColor = @{ Light = '#fbfbfb'; Dark = '#393939' }
DisabledColor = @{ Light = '#f7f7f7'; Dark = '#3b3b3b' }
HoverColor = @{ Light = '#f6f6f6'; Dark = '#2a2a2a' }
PressedColor = @{ Light = '#f0f0f0'; Dark = '#1e1e1e' }
TextDisabledColor = @{ Light = '#b7b7b7'; Dark = '#787878' }
}
CheckBox = @{
BgColor = @{ Light = '#f5f5f5'; Dark = '#272727' }
BorderColor = @{ Light = '#898989'; Dark = '#808080' }
HoverColor = @{ Light = '#ececec'; Dark = '#343434' }
}
ComboBox = @{
BgColor = @{ Light = '#ffffff'; Dark = '#373737' }
HoverColor = @{ Light = '#f8f8f8'; Dark = '#434343' }
ItemBgColor = @{ Light = '#f9f9f9'; Dark = '#2c2c2c' }
ItemHoverColor = @{ Light = '#f0f0f0'; Dark = '#383838' }
ItemSelectedColor = @{ Light = '#f3f3f3'; Dark = '#343434' }
}
TextBox = @{
BorderColor = @{ Light = '#bdbdbd'; Dark = '#989a9d' }
BgColor = @{ Light = '#fbfbfb'; Dark = '#2d2d2d' }
FocusColor = @{ Light = '#ffffff'; Dark = '#1f1f1f' }
HoverColor = @{ Light = '#f6f6f6'; Dark = '#323232' }
SideBorderColor = @{ Light = '#ececec'; Dark = '#343434' }
}
ScrollBar = @{
ThumbColor = @{ Light = '#b9b9b9'; Dark = '#3d3d3d' }
ThumbHoverColor = @{ Light = '#8b8b8b'; Dark = '#4b4b4b' }
}
TitleBar = @{
ButtonHoverColor = @{ Light = '#dcdcdc'; Dark = '#353535' }
ButtonPressedColor = @{ Light = '#cccccc'; Dark = '#333333' }
CloseHoverColor = @{ Light = '#e81123'; Dark = '#e81123' }
ClosePressedColor = @{ Light = '#f1707a'; Dark = '#f1707a' }
UnfocusedFgColor = @{ Light = '#868686'; Dark = '#969696' }
}
Search = @{
HighlightActiveColor = @{ Light = '#ffd966'; Dark = '#8a7000' }
HighlightColor = @{ Light = '#fff4ce'; Dark = '#4a4a2a' }
}
Table = @{
HeaderColor = @{ Light = '#ffffff'; Dark = '#303030' }
}
Icon = @{
ErrorColor = @{ Light = '#e81123'; Dark = '#e81123' }
InformationColor = @{ Light = '#0078d4'; Dark = '#0078d4' }
QuestionColor = @{ Light = '#0078d4'; Dark = '#0078d4' }
SuccessColor = @{ Light = '#107c10'; Dark = '#107c10' }
WarningColor = @{ Light = '#ffb900'; Dark = '#ffb900' }
}
}
$Theme = if ($usesDarkMode) { 'Dark' } else { 'Light' }
foreach ($Group in $ThemeColor.GetEnumerator()) {
foreach ($Resource in $Group.Value.GetEnumerator()) {
$ResourceName = $Group.Key + $Resource.Key
$window.Resources[$ResourceName] = [System.Windows.Media.SolidColorBrush]::new(
[System.Windows.Media.ColorConverter]::ConvertFromString($Resource.Value[$Theme])
)
}
}
# 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
$sharedReader = [System.Xml.XmlReader]::Create([System.IO.StringReader]::new($sharedXaml))
try {
$sharedDict = [System.Windows.Markup.XamlReader]::Load($sharedReader)
$window.Resources.MergedDictionaries.Add($sharedDict)
}
finally {
$sharedReader.Close()
}
}
}
-95
View File
@@ -1,95 +0,0 @@
# Sets resource colors for a WPF window based on dark mode preference
function SetWindowThemeResources {
param (
$window,
[bool]$usesDarkMode
)
if ($usesDarkMode) {
$window.Resources.Add("BgColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#202020")))
$window.Resources.Add("FgColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#FFFFFF")))
$window.Resources.Add("CardBgColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#2b2b2b")))
$window.Resources.Add("BorderColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#404040")))
$window.Resources.Add("ButtonBorderColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#404040")))
$window.Resources.Add("CheckBoxBgColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#272727")))
$window.Resources.Add("CheckBoxBorderColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#808080")))
$window.Resources.Add("CheckBoxHoverColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#343434")))
$window.Resources.Add("ComboBgColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#373737")))
$window.Resources.Add("ComboHoverColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#434343")))
$window.Resources.Add("ComboItemBgColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#2c2c2c")))
$window.Resources.Add("ComboItemHoverColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#383838")))
$window.Resources.Add("ComboItemSelectedColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#343434")))
$window.Resources.Add("AccentColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#FFD700")))
$window.Resources.Add("ButtonDisabled", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#434343")))
$window.Resources.Add("ButtonTextDisabled", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#989898")))
$window.Resources.Add("SecondaryButtonBg", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#393939")))
$window.Resources.Add("SecondaryButtonHover", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#2a2a2a")))
$window.Resources.Add("SecondaryButtonPressed", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#1e1e1e")))
$window.Resources.Add("SecondaryButtonDisabled", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#3b3b3b")))
$window.Resources.Add("SecondaryButtonTextDisabled", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#787878")))
$window.Resources.Add("InputFocusColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#1f1f1f")))
$window.Resources.Add("ScrollBarThumbColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#3d3d3d")))
$window.Resources.Add("ScrollBarThumbHoverColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#4b4b4b")))
$window.Resources.Add("TitlebarButtonHover", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#2d2d2d")))
$window.Resources.Add("TitlebarButtonPressed", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#292929")))
$window.Resources.Add("AppIdColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#afafaf")))
$window.Resources.Add("SearchHighlightColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#4A4A2A")))
$window.Resources.Add("SearchHighlightActiveColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#8A7000")))
$window.Resources.Add("TableHeaderColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#333333")))
}
else {
$window.Resources.Add("BgColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#f3f3f3")))
$window.Resources.Add("FgColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#000000")))
$window.Resources.Add("CardBgColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#fbfbfb")))
$window.Resources.Add("BorderColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#ededed")))
$window.Resources.Add("ButtonBorderColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#d3d3d3")))
$window.Resources.Add("CheckBoxBgColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#f5f5f5")))
$window.Resources.Add("CheckBoxBorderColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#898989")))
$window.Resources.Add("CheckBoxHoverColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#ececec")))
$window.Resources.Add("ComboBgColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#FFFFFF")))
$window.Resources.Add("ComboHoverColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#f8f8f8")))
$window.Resources.Add("ComboItemBgColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#f9f9f9")))
$window.Resources.Add("ComboItemHoverColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#f0f0f0")))
$window.Resources.Add("ComboItemSelectedColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#f3f3f3")))
$window.Resources.Add("AccentColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#ffae00")))
$window.Resources.Add("ButtonDisabled", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#bfbfbf")))
$window.Resources.Add("ButtonTextDisabled", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#ffffff")))
$window.Resources.Add("SecondaryButtonBg", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#fbfbfb")))
$window.Resources.Add("SecondaryButtonHover", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#f6f6f6")))
$window.Resources.Add("SecondaryButtonPressed", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#f0f0f0")))
$window.Resources.Add("SecondaryButtonDisabled", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#f7f7f7")))
$window.Resources.Add("SecondaryButtonTextDisabled", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#b7b7b7")))
$window.Resources.Add("InputFocusColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#fbfbfb")))
$window.Resources.Add("ScrollBarThumbColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#b9b9b9")))
$window.Resources.Add("ScrollBarThumbHoverColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#8b8b8b")))
$window.Resources.Add("TitlebarButtonHover", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#e1e1e1")))
$window.Resources.Add("TitlebarButtonPressed", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#e6e6e6")))
$window.Resources.Add("AppIdColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#666666")))
$window.Resources.Add("SearchHighlightColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#FFF4CE")))
$window.Resources.Add("SearchHighlightActiveColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#FFD966")))
$window.Resources.Add("TableHeaderColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#ffffff")))
}
$window.Resources.Add("ButtonBg", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#0067c0")))
$window.Resources.Add("ButtonHover", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#1E88E5")))
$window.Resources.Add("ButtonPressed", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#3284cc")))
$window.Resources.Add("CloseHover", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#c42b1c")))
$window.Resources.Add("InformationIconColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#0078D4")))
$window.Resources.Add("SuccessIconColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#107C10")))
$window.Resources.Add("WarningIconColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#FFB900")))
$window.Resources.Add("ErrorIconColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#E81123")))
$window.Resources.Add("QuestionIconColor", [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#0078D4")))
# Load and merge shared styles
if ($script:SharedStylesSchema -and (Test-Path $script:SharedStylesSchema)) {
$sharedXaml = Get-Content -Path $script:SharedStylesSchema -Raw
$sharedReader = [System.Xml.XmlReader]::Create([System.IO.StringReader]::new($sharedXaml))
try {
$sharedDict = [System.Windows.Markup.XamlReader]::Load($sharedReader)
$window.Resources.MergedDictionaries.Add($sharedDict)
}
finally {
$sharedReader.Close()
}
}
}
+10 -3
View File
@@ -1,3 +1,10 @@
<#
.SYNOPSIS
Displays the themed About dialog for the application.
.PARAMETER Owner
The optional window that owns the dialog and its modal overlay.
#>
function Show-AboutDialog {
param (
[Parameter(Mandatory=$false)]
@@ -6,7 +13,7 @@ function Show-AboutDialog {
Add-Type -AssemblyName PresentationFramework,PresentationCore,WindowsBase | Out-Null
$usesDarkMode = GetSystemUsesDarkMode
$usesDarkMode = Get-SystemUsesDarkMode
# Determine owner window
$ownerWindow = if ($Owner) { $Owner } else { $script:GuiWindow }
@@ -42,7 +49,7 @@ function Show-AboutDialog {
}
# Apply theme resources
SetWindowThemeResources -window $aboutWindow -usesDarkMode $usesDarkMode
Set-WindowThemeResources -window $aboutWindow -usesDarkMode $usesDarkMode
# Get UI elements
$titleBar = $aboutWindow.FindName('TitleBar')
@@ -95,4 +102,4 @@ function Show-AboutDialog {
catch { }
}
}
}
}
+27 -15
View File
@@ -1,8 +1,14 @@
# Shows application selection window that allows the user to select what apps they want to remove or keep
<#
.SYNOPSIS
Displays the application-selection dialog and records the confirmed selections.
.OUTPUTS
System.Nullable[System.Boolean]. The dialog result; confirmed application IDs are stored in $script:SelectedApps.
#>
function Show-AppSelectionWindow {
Add-Type -AssemblyName PresentationFramework,PresentationCore,WindowsBase | Out-Null
$usesDarkMode = GetSystemUsesDarkMode
$usesDarkMode = Get-SystemUsesDarkMode
# Show overlay if main window exists
$overlay = $null
@@ -34,7 +40,7 @@ function Show-AppSelectionWindow {
catch { }
}
SetWindowThemeResources -window $window -usesDarkMode $usesDarkMode
Set-WindowThemeResources -window $window -usesDarkMode $usesDarkMode
$appsPanel = $window.FindName('AppsPanel')
$checkAllBox = $window.FindName('CheckAllBox')
@@ -46,26 +52,32 @@ function Show-AppSelectionWindow {
# Track the last selected checkbox for shift-click range selection
$script:AppSelectionWindowLastSelectedCheckbox = $null
# Loads apps into the apps UI
function LoadApps {
<#
.SYNOPSIS
Reloads the application-selection checkboxes using the current installed-apps filter.
.NOTES
Updates the dialog loading indicator and resets range-selection state.
#>
function Load-Apps {
# Show loading indicator
$loadingIndicator.Visibility = 'Visible'
$window.Dispatcher.Invoke([System.Windows.Threading.DispatcherPriority]::Background, [action]{})
$appsPanel.Children.Clear()
$listOfApps = ""
$listOfApps = $null
if ($onlyInstalledBox.IsChecked -and ($script:WingetInstalled -eq $true)) {
# Attempt to get a list of installed apps via WinGet, times out after 10 seconds
$listOfApps = GetInstalledAppsViaWinget -TimeOut 10
if (-not $listOfApps) {
$listOfApps = Get-WingetInstalledApps -TimeOut 10 -NonBlocking
if ($null -eq $listOfApps) {
# Show error that the script was unable to get list of apps from WinGet
Show-MessageBox -Message 'Unable to load list of installed apps via WinGet.' -Title 'Error' -Button 'OK' -Icon 'Error' -Owner $window | Out-Null
$onlyInstalledBox.IsChecked = $false
}
}
$appsToAdd = LoadAppsDetailsFromJson -OnlyInstalled:$onlyInstalledBox.IsChecked -InstalledList $listOfApps -InitialCheckedFromJson:$true
$appsToAdd = Import-AppDetailsFromJson -OnlyInstalled:$onlyInstalledBox.IsChecked -InstalledList $listOfApps -InitialCheckedFromJson:$true
# Reset the last selected checkbox when loading a new list
$script:AppSelectionWindowLastSelectedCheckbox = $null
@@ -82,7 +94,7 @@ function Show-AppSelectionWindow {
$checkbox.Style = $window.Resources["AppsPanelCheckBoxStyle"]
# Attach shift-click behavior for range selection
AttachShiftClickBehavior -checkbox $checkbox -appsPanel $appsPanel -lastSelectedCheckboxRef ([ref]$script:AppSelectionWindowLastSelectedCheckbox)
Attach-ShiftClickBehavior -checkbox $checkbox -appsPanel $appsPanel -lastSelectedCheckboxRef ([ref]$script:AppSelectionWindowLastSelectedCheckbox)
$appsPanel.Children.Add($checkbox) | Out-Null
}
@@ -112,8 +124,8 @@ function Show-AppSelectionWindow {
}
})
$onlyInstalledBox.Add_Checked({ LoadApps })
$onlyInstalledBox.Add_Unchecked({ LoadApps })
$onlyInstalledBox.Add_Checked({ Load-Apps })
$onlyInstalledBox.Add_Unchecked({ Load-Apps })
$confirmBtn.Add_Click({
$selectedApps = @()
@@ -130,18 +142,18 @@ function Show-AppSelectionWindow {
return
}
if (-not (ConfirmUnsafeAppRemoval -SelectedApps $selectedApps -Owner $window)) {
if (-not (Confirm-UnsafeAppRemoval -SelectedApps $selectedApps -Owner $window)) {
return
}
SaveCustomAppsListToFile -appsList $selectedApps
$script:SelectedApps = $selectedApps
$window.DialogResult = $true
})
# Load apps after window is shown (allows UI to render first)
$window.Add_ContentRendered({
$window.Dispatcher.Invoke([System.Windows.Threading.DispatcherPriority]::Background, [action]{ LoadApps }) | Out-Null
$window.Dispatcher.Invoke([System.Windows.Threading.DispatcherPriority]::Background, [action]{ Load-Apps }) | Out-Null
})
# Show the window and return dialog result
+42 -49
View File
@@ -1,34 +1,24 @@
<#
.SYNOPSIS
Displays the modal progress window while selected changes are applied.
.PARAMETER Owner
The optional window that owns the modal and its overlay.
.PARAMETER InvokeRestartExplorer
Indicates whether the modal should run the Explorer-restart flow after applying changes.
#>
function Show-ApplyModal {
param (
[Parameter(Mandatory=$false)]
[System.Windows.Window]$Owner = $null,
[Parameter(Mandatory=$false)]
[bool]$RestartExplorer = $false
[bool]$InvokeRestartExplorer = $false
)
Add-Type -AssemblyName PresentationFramework,PresentationCore,WindowsBase | Out-Null
# P/Invoke helpers for forcing focus back after Explorer restart
if (-not ([System.Management.Automation.PSTypeName]'Win11Debloat.FocusHelper').Type) {
Add-Type -Namespace Win11Debloat -Name FocusHelper -MemberDefinition @'
[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hWnd);
[DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr hWnd, IntPtr lpdwProcessId);
[DllImport("user32.dll")] public static extern bool AttachThreadInput(uint idAttach, uint idAttachTo, bool fAttach);
[DllImport("kernel32.dll")] public static extern uint GetCurrentThreadId();
public static void ForceActivate(IntPtr hwnd) {
IntPtr fg = GetForegroundWindow();
uint fgThread = GetWindowThreadProcessId(fg, IntPtr.Zero);
uint myThread = GetCurrentThreadId();
if (fgThread != myThread) AttachThreadInput(myThread, fgThread, true);
SetForegroundWindow(hwnd);
if (fgThread != myThread) AttachThreadInput(myThread, fgThread, false);
}
'@
}
$usesDarkMode = GetSystemUsesDarkMode
$usesDarkMode = Get-SystemUsesDarkMode
# Determine owner window
$ownerWindow = if ($Owner) { $Owner } else { $script:GuiWindow }
@@ -64,7 +54,7 @@ function Show-ApplyModal {
}
# Apply theme resources
SetWindowThemeResources -window $applyWindow -usesDarkMode $usesDarkMode
Set-WindowThemeResources -window $applyWindow -usesDarkMode $usesDarkMode
# Get UI elements
$script:ApplyInProgressPanel = $applyWindow.FindName('ApplyInProgressPanel')
@@ -89,7 +79,7 @@ function Show-ApplyModal {
$script:ApplyProgressBarEl.Value = 0
$script:ApplyModalInErrorState = $false
# Set up progress callback for ExecuteAllChanges
# Set up progress callback for Invoke-AllChanges
$script:ApplyProgressCallback = {
param($currentStep, $totalSteps, $stepName)
$script:ApplyStepNameEl.Text = $stepName
@@ -101,7 +91,7 @@ function Show-ApplyModal {
$pct = if ($totalSteps -gt 0) { [math]::Round((($currentStep - 1) / $totalSteps) * 100) } else { 0 }
$script:ApplyProgressBarEl.Value = $pct
# Process pending window messages to keep UI responsive
DoEvents
Invoke-DoEvents
}
# Sub-step callback updates step name and interpolates progress bar within the current step
@@ -116,34 +106,29 @@ function Show-ApplyModal {
$stepFraction = ($subIndex / $subCount) / $totalSteps
$script:ApplyProgressBarEl.Value = [math]::Round(($baseProgress + $stepFraction) * 100)
}
DoEvents
Invoke-DoEvents
}
# Run changes in background to keep UI responsive
$applyWindow.Dispatcher.BeginInvoke([System.Windows.Threading.DispatcherPriority]::Background, [action]{
try {
ExecuteAllChanges
Invoke-AllChanges
$registryImportFailureCount = [int]$script:RegistryImportFailures
$failureCount = [int]$script:FeatureFailures + [int]$script:AppRemovalFailures
$appRemovalVerificationUnavailable = [bool]$script:AppRemovalVerificationUnavailable
# Restart explorer if requested
if ($RestartExplorer -and -not $script:CancelRequested) {
RestartExplorer
if ($InvokeRestartExplorer -and -not $script:CancelRequested) {
Invoke-RestartExplorer
# Wait for Explorer to finish relaunching, then reclaim focus.
Start-Sleep -Milliseconds 800
$applyWindow.Dispatcher.Invoke([action]{
$hwnd = (New-Object System.Windows.Interop.WindowInteropHelper($applyWindow)).Handle
[Win11Debloat.FocusHelper]::ForceActivate($hwnd)
$applyWindow.Activate()
})
}
Write-Host ""
if ($script:CancelRequested) {
Write-Host "Script execution was cancelled by the user. Some changes may not have been applied."
} elseif ($registryImportFailureCount -eq 0) {
Write-Host "All changes have been applied successfully!"
}
# Show completion state
$script:ApplyProgressBarEl.Value = 100
@@ -151,34 +136,42 @@ function Show-ApplyModal {
$script:ApplyCompletionPanel.Visibility = 'Visible'
if ($script:CancelRequested) {
Write-Warning "Script execution was cancelled by the user. Any remaining changes were not applied."
$script:ApplyCompletionIconEl.Text = [char]0xE7BA
$script:ApplyCompletionIconEl.Foreground = [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#e8912d"))
$script:ApplyCompletionTitleEl.Text = "Cancelled"
$script:ApplyCompletionMessageEl.Text = "Script execution was cancelled by the user."
} elseif ($registryImportFailureCount -gt 0) {
} elseif ($failureCount -gt 0 -or $appRemovalVerificationUnavailable) {
if ($failureCount -gt 0) {
Write-Host "Script completed with $failureCount error(s)."
}
$script:ApplyCompletionIconEl.Text = [char]0xE7BA
$script:ApplyCompletionIconEl.Foreground = [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#e8912d"))
$script:ApplyCompletionTitleEl.Text = "Changes Applied with Errors"
$script:ApplyCompletionMessageEl.Text = "$registryImportFailureCount registry change(s) failed. See console for details."
if ($failureCount -eq 0 -and $appRemovalVerificationUnavailable) {
$script:ApplyCompletionTitleEl.Text = "Changes Applied"
$script:ApplyCompletionMessageEl.Text = "All changes were applied without errors, but Win11Debloat could not confirm that all selected apps were successfully uninstalled."
}
else {
$script:ApplyCompletionTitleEl.Text = "Changes Applied with Errors"
$script:ApplyCompletionMessageEl.Text = "$failureCount change(s) failed. See console for details."
}
} else {
Write-Host "All changes have been applied successfully!"
$script:ApplyCompletionTitleEl.Text = "Changes Applied"
# 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)"
}
}
if ($InvokeRestartExplorer) {
$rebootFeatures = Get-RebootFeatureLabels
if ($rebootFeatures.Count -gt 0) {
foreach ($featureName in $rebootFeatures) {
$tb = [System.Windows.Controls.TextBlock]::new()
$tb.Text = "$([char]0x2022) $featureName"
$tb.FontSize = 12
$tb.SetResourceReference([System.Windows.Controls.TextBlock]::ForegroundProperty, 'FgColor')
$tb.SetResourceReference([System.Windows.Controls.TextBlock]::ForegroundProperty, "AppFgColor")
$tb.Opacity = 0.85
$tb.Margin = [System.Windows.Thickness]::new(0, 2, 0, 0)
$applyRebootList.Children.Add($tb) | Out-Null
+48
View File
@@ -1,3 +1,23 @@
<#
.SYNOPSIS
Hides the currently displayed bubble popup.
.DESCRIPTION
Closes the bubble popup with a smooth fade-out animation (220ms). If the
-Immediate switch is used, the popup is closed instantly without animation.
This function is called automatically by Show-Bubble's timer and can also
be invoked manually to dismiss the bubble early.
.PARAMETER Immediate
If specified, the bubble popup is closed instantly without a fade-out
animation. Any pending close timer is also stopped.
.EXAMPLE
Hide-Bubble
.EXAMPLE
Hide-Bubble -Immediate
#>
function Hide-Bubble {
param (
[Parameter(Mandatory=$false)]
@@ -37,6 +57,34 @@ function Hide-Bubble {
$bubblePanel.BeginAnimation([System.Windows.UIElement]::OpacityProperty, $fadeOut)
}
<#
.SYNOPSIS
Displays a transient bubble popup hint anchored above a target control.
.DESCRIPTION
Shows a WPF popup styled as a speech bubble above the specified target
control. The bubble fades in with a animation, displays for a configurable
duration, then fades out. Any previously shown bubble is dismissed
immediately before showing the new one.
.PARAMETER TargetControl
The WPF Control above which the bubble popup will be placed. This
parameter is mandatory.
.PARAMETER Message
The text message to display inside the bubble. Defaults to
'View the selected changes here'.
.PARAMETER DurationSeconds
The number of seconds the bubble remains visible before auto-hiding.
The minimum value is 1 second. Defaults to 5 seconds.
.EXAMPLE
Show-Bubble -TargetControl $myButton
.EXAMPLE
Show-Bubble -TargetControl $myButton -Message 'Changes saved!' -DurationSeconds 3
#>
function Show-Bubble {
param (
[Parameter(Mandatory=$true)]
@@ -1,3 +1,7 @@
<#
.SYNOPSIS
Shows a modal category-selection dialog for importing or exporting configuration.
#>
function Show-ImportExportConfigWindow {
param (
[System.Windows.Window]$Owner,
@@ -21,7 +25,8 @@ function Show-ImportExportConfigWindow {
$Owner.Dispatcher.Invoke([action]{ $overlay.Visibility = 'Visible' })
}
}
} catch { }
}
catch { }
# Load XAML from schema file
$schemaPath = $script:ImportExportConfigSchema
@@ -44,7 +49,7 @@ function Show-ImportExportConfigWindow {
}
$dlg.Owner = $Owner
SetWindowThemeResources -window $dlg -usesDarkMode $UsesDarkMode
Set-WindowThemeResources -window $dlg -usesDarkMode $UsesDarkMode
# Copy the CheckBox default style from the main window so checkboxes get the themed template
try {
@@ -52,7 +57,8 @@ function Show-ImportExportConfigWindow {
if ($mainCheckBoxStyle) {
$dlg.Resources.Add([type][System.Windows.Controls.CheckBox], $mainCheckBoxStyle)
}
} catch { }
}
catch { }
# Populate named elements
$dlg.Title = $Title
@@ -78,7 +84,7 @@ function Show-ImportExportConfigWindow {
$cb.Margin = [System.Windows.Thickness]::new(0,0,0,4)
$cb.FontSize = 14
$cb.FontWeight = [System.Windows.FontWeights]::Medium
$cb.Foreground = $dlg.FindResource('FgColor')
$cb.Foreground = $dlg.FindResource("AppFgColor")
if ($DisabledCategories -contains $cat) {
$cb.IsChecked = $false
$cb.IsEnabled = $false
@@ -93,9 +99,9 @@ function Show-ImportExportConfigWindow {
$detailsText = New-Object System.Windows.Controls.TextBlock
$detailsText.Text = $CategoryDetails[$cat]
$detailsText.FontSize = 12
$detailsText.Foreground = $dlg.FindResource('FgColor')
$detailsText.Margin = [System.Windows.Thickness]::new(30,0,0,0)
$detailsText.Opacity = 0.75
$detailsText.Foreground = $dlg.FindResource("AppFgColor")
$detailsText.Margin = [System.Windows.Thickness]::new(32,0,0,0)
$detailsText.Opacity = if ($DisabledCategories -contains $cat) { 0.45 } else { 0.75 }
$detailsText.TextWrapping = [System.Windows.TextWrapping]::Wrap
$container.Children.Add($detailsText) | Out-Null
}
@@ -213,6 +219,11 @@ function Get-DeploymentSettings {
$deploySettings += @{ Name = 'CreateRestorePoint'; Value = [bool]$restorePointCheckBox.IsChecked }
}
$registryBackupCheckBox = $Owner.FindName('RegistryBackupCheckBox')
if ($registryBackupCheckBox) {
$deploySettings += @{ Name = 'SkipRegistryBackup'; Value = -not [bool]$registryBackupCheckBox.IsChecked }
}
$restartExplorerCheckBox = $Owner.FindName('RestartExplorerCheckBox')
if ($restartExplorerCheckBox) {
$deploySettings += @{ Name = 'RestartExplorer'; Value = [bool]$restartExplorerCheckBox.IsChecked }
@@ -266,6 +277,7 @@ function Get-DeploymentCategoryDetailString {
$options = @()
if ($lookup.ContainsKey('CreateRestorePoint') -and [bool]$lookup['CreateRestorePoint']) { $options += 'Restore Point' }
if (-not ($lookup.ContainsKey('SkipRegistryBackup') -and [bool]$lookup['SkipRegistryBackup'])) { $options += 'Registry Backup' }
if ($lookup.ContainsKey('RestartExplorer') -and [bool]$lookup['RestartExplorer']) { $options += 'Restart Explorer' }
$lines = @()
@@ -286,11 +298,17 @@ function Build-CategoryDetails {
$details = @{}
if ($AppCount -gt 0) {
$details['Applications'] = "$AppCount app$(if ($AppCount -ne 1) { 's' })"
$details['Applications'] = "$AppCount app$(if ($AppCount -ne 1) { 's' }) selected"
}
else {
$details['Applications'] = 'No apps selected'
}
if ($TweakCount -gt 0) {
$details['System Tweaks'] = "$TweakCount tweak$(if ($TweakCount -ne 1) { 's' })"
$details['System Tweaks'] = "$TweakCount tweak$(if ($TweakCount -ne 1) { 's' }) selected"
}
else {
$details['System Tweaks'] = 'No tweaks selected'
}
if ($DeploymentSettings) {
@@ -300,7 +318,11 @@ function Build-CategoryDetails {
return $details
}
function Apply-ImportedApplications {
<#
.SYNOPSIS
Applies imported application selections to the application checkboxes.
#>
function Set-ImportedApplications {
param (
[System.Windows.Controls.Panel]$AppsPanel,
[string[]]$AppIds
@@ -313,7 +335,11 @@ function Apply-ImportedApplications {
}
}
function Apply-ImportedTweakSettings {
<#
.SYNOPSIS
Applies imported tweak settings to their mapped UI controls.
#>
function Set-ImportedTweakSettings {
param (
[System.Windows.Window]$Owner,
[hashtable]$UiControlMappings,
@@ -321,10 +347,14 @@ function Apply-ImportedTweakSettings {
)
$settingsJson = [PSCustomObject]@{ Settings = @($TweakSettings) }
ApplySettingsToUiControls -window $Owner -settingsJson $settingsJson -uiControlMappings $UiControlMappings
Apply-SettingsToUiControls -window $Owner -settingsJson $settingsJson -uiControlMappings $UiControlMappings
}
function Apply-ImportedDeploymentSettings {
<#
.SYNOPSIS
Applies imported deployment settings to the deployment controls.
#>
function Set-ImportedDeploymentSettings {
param (
[System.Windows.Window]$Owner,
[System.Windows.Controls.ComboBox]$UserSelectionCombo,
@@ -354,12 +384,23 @@ function Apply-ImportedDeploymentSettings {
$restorePointCheckBox.IsChecked = [bool]$lookup['CreateRestorePoint']
}
$registryBackupCheckBox = $Owner.FindName('RegistryBackupCheckBox')
if ($registryBackupCheckBox) {
if ($lookup.ContainsKey('SkipRegistryBackup')) {
$registryBackupCheckBox.IsChecked = -not [bool]$lookup['SkipRegistryBackup']
}
}
$restartExplorerCheckBox = $Owner.FindName('RestartExplorerCheckBox')
if ($lookup.ContainsKey('RestartExplorer') -and $restartExplorerCheckBox) {
$restartExplorerCheckBox.IsChecked = [bool]$lookup['RestartExplorer']
}
}
<#
.SYNOPSIS
Exports selected application, tweak, and deployment settings to a configuration file.
#>
function Export-Configuration {
param (
[System.Windows.Window]$Owner,
@@ -381,7 +422,7 @@ function Export-Configuration {
$deploymentSettings = Get-DeploymentSettings -Owner $Owner -UserSelectionCombo $UserSelectionCombo -OtherUsernameTextBox $OtherUsernameTextBox
$categoryDetails = Build-CategoryDetails -AppCount $selectedApps.Count -TweakCount $tweakSettings.Count -DeploymentSettings $deploymentSettings
$categories = Show-ImportExportConfigWindow -Owner $Owner -UsesDarkMode $UsesDarkMode -Title 'Export Configuration' -Prompt 'Select the settings you wish to include in your export.' -DisabledCategories $disabledCategories -CategoryDetails $categoryDetails -ActionLabel 'Export Settings'
$categories = Show-ImportExportConfigWindow -Owner $Owner -UsesDarkMode $UsesDarkMode -Title 'Export Configuration' -Prompt 'Create a configuration file based on the currently selected settings. You can choose which settings categories you wish to include in the export.' -DisabledCategories $disabledCategories -CategoryDetails $categoryDetails -ActionLabel 'Export Settings'
if (-not $categories) {
Write-Host 'Export canceled.'
return
@@ -413,7 +454,13 @@ function Export-Configuration {
Write-Host "Exporting configuration to '$($saveDialog.FileName)'... (Categories: $($categories -join ', '))"
if (SaveToFile -Config $config -FilePath $saveDialog.FileName) {
if ($script:Params.ContainsKey("WhatIf")) {
Write-Host "[WhatIf] Export configuration to '$($saveDialog.FileName)'" -ForegroundColor Cyan
Show-MessageBox -Message "[WhatIf] Configuration would be exported to this file (no file written)." -Title 'Export Configuration' -Button 'OK' -Icon 'Information' | Out-Null
return
}
if (Save-ToFile -Config $config -FilePath $saveDialog.FileName) {
Write-Host "Configuration exported successfully: $($saveDialog.FileName)"
Show-MessageBox -Message "Configuration exported successfully." -Title 'Export Configuration' -Button 'OK' -Icon 'Information' | Out-Null
}
@@ -423,6 +470,10 @@ function Export-Configuration {
}
}
<#
.SYNOPSIS
Imports selected application, tweak, and deployment settings from a configuration file.
#>
function Import-Configuration {
param (
[System.Windows.Window]$Owner,
@@ -448,34 +499,29 @@ function Import-Configuration {
Write-Host "Importing configuration from '$($openDialog.FileName)'..."
$config = LoadJsonFile -filePath $openDialog.FileName -expectedVersion '1.0'
$config = Import-JsonFile -filePath $openDialog.FileName -expectedVersion '1.0'
if (-not $config) {
Write-Error "Failed to read configuration file '$($openDialog.FileName)'"
Show-MessageBox -Message "Failed to read configuration file" -Title 'Invalid Config' -Button 'OK' -Icon 'Error' | Out-Null
return
}
if (-not $config.Version) {
Write-Error "Invalid configuration file format: '$($openDialog.FileName)'"
Show-MessageBox -Message "Invalid configuration file format." -Title 'Invalid Config' -Button 'OK' -Icon 'Error' | Out-Null
$consistencyError = Test-ConfigConsistency -Config $config
if ($consistencyError) {
Write-Error "Invalid configuration file '$($openDialog.FileName)': $consistencyError"
Show-MessageBox -Message "Invalid configuration file: $consistencyError" -Title 'Invalid Config' -Button 'OK' -Icon 'Error' | Out-Null
return
}
$availableCategories = Get-AvailableImportExportCategories -Config $config
if ($availableCategories.Count -eq 0) {
Write-Warning "Configuration file '$($openDialog.FileName)' contains no importable data."
Show-MessageBox -Message "The selected file contains no importable data." -Title 'Invalid Config' -Button 'OK' -Icon 'Error' | Out-Null
return
}
Write-Host "Available categories in config: $($availableCategories -join ', ')"
$appCount = @($config.Apps | Where-Object { $_ -is [string] -and -not [string]::IsNullOrWhiteSpace($_) }).Count
$tweakCount = @($config.Tweaks | Where-Object { $_ -and $_.Name -and $_.Value -eq $true }).Count
$categoryDetails = Build-CategoryDetails -AppCount $appCount -TweakCount $tweakCount -DeploymentSettings @($config.Deployment)
$categories = Show-ImportExportConfigWindow -Owner $Owner -UsesDarkMode $UsesDarkMode -Title 'Import Configuration' -Prompt 'Select the settings you wish to import. You can review and modify them before they are applied.' -Categories $availableCategories -CategoryDetails $categoryDetails -ActionLabel 'Import Settings'
$categories = Show-ImportExportConfigWindow -Owner $Owner -UsesDarkMode $UsesDarkMode -Title 'Import Configuration' -Prompt 'Choose the settings categories that you wish to import. You can review and modify the imported settings before they are applied.' -Categories $availableCategories -CategoryDetails $categoryDetails -ActionLabel 'Import Settings'
if (-not $categories) {
Write-Host 'Import canceled.'
return
@@ -490,7 +536,7 @@ function Import-Configuration {
)
Write-Host "Importing $($appIds.Count) app selection(s)."
Apply-ImportedApplications -AppsPanel $AppsPanel -AppIds $appIds
Set-ImportedApplications -AppsPanel $AppsPanel -AppIds $appIds
if ($OnAppsImported) {
& $OnAppsImported
@@ -499,11 +545,11 @@ function Import-Configuration {
if ($categories -contains 'System Tweaks' -and $config.Tweaks) {
$tweakCount = @($config.Tweaks).Count
Write-Host "Importing $tweakCount tweak(s)."
Apply-ImportedTweakSettings -Owner $Owner -UiControlMappings $UiControlMappings -TweakSettings @($config.Tweaks)
Set-ImportedTweakSettings -Owner $Owner -UiControlMappings $UiControlMappings -TweakSettings @($config.Tweaks)
}
if ($categories -contains 'Deployment Settings' -and $config.Deployment) {
Write-Host 'Importing deployment settings.'
Apply-ImportedDeploymentSettings -Owner $Owner -UserSelectionCombo $UserSelectionCombo -OtherUsernameTextBox $OtherUsernameTextBox -DeploymentSettings @($config.Deployment)
Set-ImportedDeploymentSettings -Owner $Owner -UserSelectionCombo $UserSelectionCombo -OtherUsernameTextBox $OtherUsernameTextBox -DeploymentSettings @($config.Deployment)
}
Write-Host 'Configuration imported successfully.'
+60 -109
View File
@@ -1,73 +1,12 @@
function Show-MainWindow {
<#
.SYNOPSIS
Creates and displays the main Win11Debloat window.
#>
function Show-MainWindow {
Add-Type -AssemblyName PresentationFramework,PresentationCore,WindowsBase,System.Windows.Forms | Out-Null
# ---- Constrain maximized window to taskbar work area ----
if (-not ([System.Management.Automation.PSTypeName]'Win11Debloat.MaximizedWindowHelper').Type) {
Add-Type -Namespace Win11Debloat -Name MaximizedWindowHelper `
-ReferencedAssemblies 'PresentationFramework','System.Windows.Forms','System.Drawing' `
-MemberDefinition @'
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
private struct MINMAXINFO {
public POINT ptReserved, ptMaxSize, ptMaxPosition, ptMinTrackSize, ptMaxTrackSize;
}
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
private struct POINT { public int x, y; }
[System.Runtime.InteropServices.DllImport("user32.dll")]
private static extern System.IntPtr MonitorFromWindow(System.IntPtr hwnd, uint dwFlags);
[System.Runtime.InteropServices.DllImport("user32.dll", CharSet = System.Runtime.InteropServices.CharSet.Auto)]
private static extern bool GetMonitorInfo(System.IntPtr hMonitor, ref MONITORINFO lpmi);
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
private struct RECT {
public int Left, Top, Right, Bottom;
}
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential, CharSet = System.Runtime.InteropServices.CharSet.Auto)]
private struct MONITORINFO {
public int cbSize;
public RECT rcMonitor;
public RECT rcWork;
public uint dwFlags;
}
public static System.IntPtr WmGetMinMaxInfoHook(
System.IntPtr hwnd, int msg, System.IntPtr wParam, System.IntPtr lParam, ref bool handled) {
if (msg == 0x0024) { // WM_GETMINMAXINFO
var mmi = (MINMAXINFO)System.Runtime.InteropServices.Marshal.PtrToStructure(
lParam, typeof(MINMAXINFO));
const uint MONITOR_DEFAULTTONEAREST = 0x00000002;
var monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
var monitorInfo = new MONITORINFO();
monitorInfo.cbSize = System.Runtime.InteropServices.Marshal.SizeOf(typeof(MONITORINFO));
if (monitor != System.IntPtr.Zero && GetMonitorInfo(monitor, ref monitorInfo)) {
mmi.ptMaxPosition.x = monitorInfo.rcWork.Left - monitorInfo.rcMonitor.Left;
mmi.ptMaxPosition.y = monitorInfo.rcWork.Top - monitorInfo.rcMonitor.Top;
mmi.ptMaxSize.x = monitorInfo.rcWork.Right - monitorInfo.rcWork.Left;
mmi.ptMaxSize.y = monitorInfo.rcWork.Bottom - monitorInfo.rcWork.Top;
}
else {
var screen = System.Windows.Forms.Screen.FromHandle(hwnd);
var wa = screen.WorkingArea;
var bounds = screen.Bounds;
mmi.ptMaxPosition.x = wa.Left - bounds.Left;
mmi.ptMaxPosition.y = wa.Top - bounds.Top;
mmi.ptMaxSize.x = wa.Width;
mmi.ptMaxSize.y = wa.Height;
}
System.Runtime.InteropServices.Marshal.StructureToPtr(mmi, lParam, true);
}
return System.IntPtr.Zero;
}
'@
}
$WinVersion = Get-ItemPropertyValue 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' CurrentBuild
$usesDarkMode = GetSystemUsesDarkMode
$usesDarkMode = Get-SystemUsesDarkMode
# ---- Load XAML ----
$xaml = Get-Content -Path $script:MainWindowSchema -Raw
@@ -79,7 +18,7 @@
$reader.Close()
}
SetWindowThemeResources -window $window -usesDarkMode $usesDarkMode
Set-WindowThemeResources -window $window -usesDarkMode $usesDarkMode
$mainBorder = $window.FindName('MainBorder')
$titleBarBackground = $window.FindName('TitleBarBackground')
@@ -123,12 +62,6 @@
$window.Add_SourceInitialized({
& $applyInitialWindowSize
& $updateWindowChrome
$hwndHelper = New-Object System.Windows.Interop.WindowInteropHelper($window)
$hwndSource = [System.Windows.Interop.HwndSource]::FromHwnd($hwndHelper.Handle)
$hookMethod = [Win11Debloat.MaximizedWindowHelper].GetMethod('WmGetMinMaxInfoHook')
$hook = [System.Delegate]::CreateDelegate([System.Windows.Interop.HwndSourceHook], $hookMethod)
$hwndSource.AddHook($hook)
})
$window.Add_SizeChanged({
@@ -294,7 +227,7 @@
if ($importConfigBtn) { $importConfigBtn.IsEnabled = $false }
# ---- Build JSON-defined app presets ----
foreach ($preset in (LoadAppPresetsFromJson)) {
foreach ($preset in (Import-AppPresetsFromJson)) {
$checkbox = New-Object System.Windows.Controls.CheckBox
$checkbox.Content = $preset.Name
$checkbox.IsThreeState = $true
@@ -372,8 +305,8 @@
# ---- Load apps ----
$appLoadStatusCallback = { Update-AppSelectionStatus -AppsPanel $appsPanel -AppSelectionStatus $appSelectionStatus -AppRemovalScopeCombo $appRemovalScopeCombo -AppRemovalScopeSection $appRemovalScopeSection -AppRemovalScopeDescription $appRemovalScopeDescription -UserSelectionCombo $userSelectionCombo }
$onlyInstalledAppsBox.Add_Checked({ Load-AppsIntoMainUI -Window $window -AppsPanel $appsPanel -OnlyInstalledAppsBox $onlyInstalledAppsBox -LoadingAppsIndicator $loadingAppsIndicator -ImportConfigBtn $importConfigBtn })
$onlyInstalledAppsBox.Add_Unchecked({ Load-AppsIntoMainUI -Window $window -AppsPanel $appsPanel -OnlyInstalledAppsBox $onlyInstalledAppsBox -LoadingAppsIndicator $loadingAppsIndicator -ImportConfigBtn $importConfigBtn })
$onlyInstalledAppsBox.Add_Checked({ Initialize-MainWindowApps -Window $window -AppsPanel $appsPanel -OnlyInstalledAppsBox $onlyInstalledAppsBox -LoadingAppsIndicator $loadingAppsIndicator -ImportConfigBtn $importConfigBtn })
$onlyInstalledAppsBox.Add_Unchecked({ Initialize-MainWindowApps -Window $window -AppsPanel $appsPanel -OnlyInstalledAppsBox $onlyInstalledAppsBox -LoadingAppsIndicator $loadingAppsIndicator -ImportConfigBtn $importConfigBtn })
# ---- App presets popup ----
$presetsPopup.Add_Opened({
@@ -644,6 +577,7 @@
# ---- App removal scope combo ----
$appRemovalScopeCombo.Add_SelectionChanged({
Update-AppRemovalScopeDescription -AppRemovalScopeCombo $appRemovalScopeCombo -AppRemovalScopeDescription $appRemovalScopeDescription
Test-OtherUsername -Window $window -UserSelectionCombo $userSelectionCombo -OtherUsernameTextBox $otherUsernameTextBox -UsernameValidationMessage $usernameValidationMessage -AppRemovalScopeCombo $appRemovalScopeCombo | Out-Null
})
# ---- Other username text box ----
@@ -655,12 +589,12 @@
$usernameTextBoxPlaceholder.Visibility = 'Collapsed'
}
Update-UserSelectionDescription -Window $window -UserSelectionCombo $userSelectionCombo -OtherUsernameTextBox $otherUsernameTextBox -UserSelectionDescription $userSelectionDescription
Test-OtherUsername -Window $window -UserSelectionCombo $userSelectionCombo -OtherUsernameTextBox $otherUsernameTextBox -UsernameValidationMessage $usernameValidationMessage | Out-Null
Test-OtherUsername -Window $window -UserSelectionCombo $userSelectionCombo -OtherUsernameTextBox $otherUsernameTextBox -UsernameValidationMessage $usernameValidationMessage -AppRemovalScopeCombo $appRemovalScopeCombo | Out-Null
})
# ---- Validate target user helper ----
$ensureValidTargetUserOrWarn = {
if (-not (Test-OtherUsername -Window $window -UserSelectionCombo $userSelectionCombo -OtherUsernameTextBox $otherUsernameTextBox -UsernameValidationMessage $usernameValidationMessage)) {
if (-not (Test-OtherUsername -Window $window -UserSelectionCombo $userSelectionCombo -OtherUsernameTextBox $otherUsernameTextBox -UsernameValidationMessage $usernameValidationMessage -AppRemovalScopeCombo $appRemovalScopeCombo)) {
$validationMessage = if (-not [string]::IsNullOrWhiteSpace($usernameValidationMessage.Text)) {
$usernameValidationMessage.Text
}
@@ -688,9 +622,9 @@
$ShowCurrentlyAppliedTweaksCheckBox.IsChecked = $false
}
$defaultsJson = LoadJsonFile -filePath $script:DefaultSettingsFilePath -expectedVersion "1.0"
$defaultsJson = Import-JsonFile -filePath $script:DefaultSettingsFilePath -expectedVersion "1.0"
if ($defaultsJson) {
ApplySettingsToUiControls -window $window -settingsJson $defaultsJson -uiControlMappings $script:UiControlMappings
Apply-SettingsToUiControls -window $window -settingsJson $defaultsJson -uiControlMappings $script:UiControlMappings
}
if ($script:IsLoadingApps) {
@@ -734,25 +668,23 @@
$hasAppSelection = ($selectedApps.Count -gt 0)
if ($selectedApps.Count -gt 0) {
if (-not (ConfirmUnsafeAppRemoval -SelectedApps $selectedApps -Owner $window)) { return }
if (-not (Confirm-UnsafeAppRemoval -SelectedApps $selectedApps -Owner $window)) { return }
AddParameter 'RemoveApps'
AddParameter 'Apps' ($selectedApps -join ',')
$selectedScopeItem = $appRemovalScopeCombo.SelectedItem
if ($selectedScopeItem) {
switch ($selectedScopeItem.Content) {
"All users" { AddParameter 'AppRemovalTarget' 'AllUsers' }
"Current user only" { AddParameter 'AppRemovalTarget' 'CurrentUser' }
"Target user only" { AddParameter 'AppRemovalTarget' ($otherUsernameTextBox.Text.Trim()) }
}
$scopeTarget = Get-AppRemovalScopeTarget -AppRemovalScopeCombo $appRemovalScopeCombo -OtherUsernameTextBox $otherUsernameTextBox
if ([string]::IsNullOrWhiteSpace($scopeTarget)) {
Write-Warning 'App removal was cancelled because the selected removal scope is invalid.'
return
}
Add-Parameter 'RemoveApps'
Add-Parameter 'Apps' ($selectedApps -join ',')
Add-Parameter 'AppRemovalTarget' $scopeTarget
}
# Apply dynamic tweaks
foreach ($tweakAction in @(Get-PendingTweakActions -Window $window -ShowAppliedTweaksMode:$showAppliedTweaksMode)) {
if ($tweakAction.Action -eq 'Apply') {
AddParameter $tweakAction.FeatureId
Add-Parameter $tweakAction.FeatureId
$null = $selectedForwardFeatureIds.Add([string]$tweakAction.FeatureId)
continue
}
@@ -766,27 +698,32 @@
$restorePointCheckBox = $window.FindName('RestorePointCheckBox')
if ($restorePointCheckBox -and $restorePointCheckBox.IsChecked) {
AddParameter 'CreateRestorePoint'
Add-Parameter 'CreateRestorePoint'
}
$registryBackupCheckBox = $window.FindName('RegistryBackupCheckBox')
if ($registryBackupCheckBox -and -not $registryBackupCheckBox.IsChecked) {
Add-Parameter 'SkipRegistryBackup'
}
switch ($userSelectionCombo.SelectedIndex) {
0 { Write-Host "Selected user mode: current user ($(GetUserName))" }
0 { Write-Host "Selected user mode: current user ($(Get-UserName))" }
1 {
Write-Host "Selected user mode: $($otherUsernameTextBox.Text.Trim())"
AddParameter User ($otherUsernameTextBox.Text.Trim())
Add-Parameter User ($otherUsernameTextBox.Text.Trim())
}
2 {
Write-Host "Selected user mode: default user profile (Sysprep)"
AddParameter Sysprep
Add-Parameter Sysprep
}
}
SaveSettings
Save-Settings
$restartExplorerCheckBox = $window.FindName('RestartExplorerCheckBox')
$shouldRestartExplorer = $restartExplorerCheckBox -and $restartExplorerCheckBox.IsChecked
Show-ApplyModal -Owner $window -RestartExplorer $shouldRestartExplorer
Show-ApplyModal -Owner $window -InvokeRestartExplorer $shouldRestartExplorer
$window.Close()
})
@@ -808,12 +745,12 @@
$window.Add_Loaded({
try {
& $updateHomeContentPosition
Build-DynamicTweaks -Window $window -WinVersion $WinVersion
Load-CurrentTweakStateIntoUI -Window $window
New-DynamicTweakControls -Window $window -WinVersion $WinVersion
Set-CurrentTweakStateInUi -Window $window
Update-TweaksResponsiveColumns -Window $window
$lastUsedSettingsJson = LoadJsonFile -filePath $script:SavedSettingsFilePath -expectedVersion "1.0" -optionalFile
$defaultsJson = LoadJsonFile -filePath $script:DefaultSettingsFilePath -expectedVersion "1.0"
$lastUsedSettingsJson = Import-JsonFile -filePath $script:SavedSettingsFilePath -expectedVersion "1.0" -optionalFile
$defaultsJson = Import-JsonFile -filePath $script:DefaultSettingsFilePath -expectedVersion "1.0"
$script:SavedAppIds = Get-SavedAppIdsFromSettingsJson -SettingsJson $lastUsedSettingsJson
@@ -821,13 +758,13 @@
Register-TweakPresetControlStateHandlers -Window $window
Update-TweakPresetStates -Window $window
Load-AppsIntoMainUI -Window $window -AppsPanel $appsPanel -OnlyInstalledAppsBox $onlyInstalledAppsBox -LoadingAppsIndicator $loadingAppsIndicator -ImportConfigBtn $importConfigBtn
Initialize-MainWindowApps -Window $window -AppsPanel $appsPanel -OnlyInstalledAppsBox $onlyInstalledAppsBox -LoadingAppsIndicator $loadingAppsIndicator -ImportConfigBtn $importConfigBtn
# Update Current User label
if ($userSelectionCombo -and $userSelectionCombo.Items.Count -gt 0) {
$currentUserItem = $userSelectionCombo.Items[0]
if ($currentUserItem -is [System.Windows.Controls.ComboBoxItem]) {
$currentUserItem.Content = "Current User ($(GetUserName))"
$currentUserItem.Content = "Current User ($(Get-UserName))"
}
}
@@ -844,11 +781,17 @@
}
$restartExplorerCheckBox = $window.FindName('RestartExplorerCheckBox')
if ($restartExplorerCheckBox -and $script:Params.ContainsKey("NoRestartExplorer")) {
if ($restartExplorerCheckBox -and $script:Params.ContainsKey('SkipExplorerRestart')) {
$restartExplorerCheckBox.IsChecked = $false
$restartExplorerCheckBox.IsEnabled = $false
}
$registryBackupCheckBox = $window.FindName('RegistryBackupCheckBox')
if ($registryBackupCheckBox -and $script:Params.ContainsKey('SkipRegistryBackup')) {
$registryBackupCheckBox.IsChecked = $false
$registryBackupCheckBox.IsEnabled = $false
}
if ($script:Params.ContainsKey("Sysprep")) {
$userSelectionCombo.SelectedIndex = 2
$userSelectionCombo.IsEnabled = $false
@@ -880,8 +823,8 @@
})
# ---- Tweak presets wiring ----
$lastUsedSettingsJson = LoadJsonFile -filePath $script:SavedSettingsFilePath -expectedVersion "1.0" -optionalFile
$defaultsJson = LoadJsonFile -filePath $script:DefaultSettingsFilePath -expectedVersion "1.0"
$lastUsedSettingsJson = Import-JsonFile -filePath $script:SavedSettingsFilePath -expectedVersion "1.0" -optionalFile
$defaultsJson = Import-JsonFile -filePath $script:DefaultSettingsFilePath -expectedVersion "1.0"
$script:DefaultTweakPresetMap = @{}
$script:LastUsedTweakPresetMap = @{}
$script:PrivacyTweakPresetMap = @{}
@@ -940,7 +883,7 @@
# ---- Preload app data ----
try {
$script:PreloadedAppData = LoadAppsDetailsFromJson -OnlyInstalled:$false -InstalledList '' -InitialCheckedFromJson:$false
$script:PreloadedAppData = Import-AppDetailsFromJson -OnlyInstalled:$false -InstalledList $null -InitialCheckedFromJson:$false
}
catch {
Write-Warning "Failed to preload apps list: $_"
@@ -953,6 +896,14 @@
})
$window.Show() | Out-Null
# If WhatIf mode is enabled, notify the user that no changes will be made
if ($script:Params.ContainsKey("WhatIf")) {
$window.Dispatcher.BeginInvoke([System.Windows.Threading.DispatcherPriority]::Loaded, [action]{
Show-MessageBox -Message "WhatIf mode is enabled. The script will not make any changes to your system in this mode.`n`nYou can observe the actions that would be taken by the script in the console output." -Title 'WhatIf Mode' -Button 'OK' -Icon 'Information' -Owner $window
}) | Out-Null
}
[System.Windows.Threading.Dispatcher]::PushFrame($frame)
return $null
}
+13 -8
View File
@@ -1,4 +1,7 @@
# Shows a Windows 11 styled custom message box
<#
.SYNOPSIS
Shows a themed Windows 11-style message box.
#>
function Show-MessageBox {
param (
[Parameter(Mandatory=$true)]
@@ -24,7 +27,7 @@ function Show-MessageBox {
Add-Type -AssemblyName PresentationFramework,PresentationCore,WindowsBase | Out-Null
$usesDarkMode = GetSystemUsesDarkMode
$usesDarkMode = Get-SystemUsesDarkMode
# Determine owner window - use provided Owner, or fall back to main GUI window
$ownerWindow = if ($Owner) { $Owner } else { $script:GuiWindow }
@@ -69,7 +72,7 @@ function Show-MessageBox {
}
# Apply theme resources
SetWindowThemeResources -window $msgWindow -usesDarkMode $usesDarkMode
Set-WindowThemeResources -window $msgWindow -usesDarkMode $usesDarkMode
# Get UI elements
$titleText = $msgWindow.FindName('TitleText')
@@ -87,27 +90,27 @@ function Show-MessageBox {
switch ($Icon) {
'Information' {
$iconText.Text = [char]0xE946
$iconText.Foreground = $msgWindow.FindResource('InformationIconColor')
$iconText.Foreground = $msgWindow.FindResource('IconInformationColor')
$iconText.Visibility = 'Visible'
}
'Success' {
$iconText.Text = [char]0xE73E
$iconText.Foreground = $msgWindow.FindResource('SuccessIconColor')
$iconText.Foreground = $msgWindow.FindResource('IconSuccessColor')
$iconText.Visibility = 'Visible'
}
'Warning' {
$iconText.Text = [char]0xE7BA
$iconText.Foreground = $msgWindow.FindResource('WarningIconColor')
$iconText.Foreground = $msgWindow.FindResource('IconWarningColor')
$iconText.Visibility = 'Visible'
}
'Error' {
$iconText.Text = [char]0xEA39
$iconText.Foreground = $msgWindow.FindResource('ErrorIconColor')
$iconText.Foreground = $msgWindow.FindResource('IconErrorColor')
$iconText.Visibility = 'Visible'
}
'Question' {
$iconText.Text = [char]0xE897
$iconText.Foreground = $msgWindow.FindResource('QuestionIconColor')
$iconText.Foreground = $msgWindow.FindResource('IconQuestionColor')
$iconText.Visibility = 'Visible'
}
default {
@@ -121,6 +124,8 @@ function Show-MessageBox {
$button1.Content = 'OK'
$button1.Add_Click({ $msgWindow.Tag = 'OK'; $msgWindow.Close() })
$button2.Visibility = 'Collapsed'
# Right-align sole button by moving it to column 1
[System.Windows.Controls.Grid]::SetColumn($button1, 1)
}
'OKCancel' {
$button1.Content = 'OK'
+46 -27
View File
@@ -1,12 +1,32 @@
<#
.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> }
Restore-StartMenu - @{ Result='Restore-StartMenu'; StartMenuScope=<scope>;
UseManualBackupFile=<bool>; BackupFilePath=<path|string> }
Cancelled - @{ Result='Cancelled' } (from New-RestoreDialogState)
#>
function Show-RestoreBackupDialog {
param(
[Parameter(Mandatory = $false)]
[System.Windows.Window]$Owner = $null
)
Add-Type -AssemblyName PresentationFramework,PresentationCore,WindowsBase | Out-Null
$usesDarkMode = GetSystemUsesDarkMode
$usesDarkMode = Get-SystemUsesDarkMode
$ownerWindow = if ($Owner) { $Owner } else { $script:GuiWindow }
$overlay = $null
@@ -47,7 +67,7 @@ function Show-RestoreBackupDialog {
}
try {
SetWindowThemeResources -window $window -usesDarkMode $usesDarkMode
Set-WindowThemeResources -window $window -usesDarkMode $usesDarkMode
}
catch { }
@@ -109,7 +129,7 @@ function Show-RestoreBackupDialog {
param([string]$BackupFilePath)
$scopeInfo = & $getStartMenuScopeInfo
$backupTargetText.Text = GetFriendlyRegistryBackupTarget -Target $scopeInfo.Target
$backupTargetText.Text = Get-FriendlyRegistryBackupTarget -Target $scopeInfo.Target
$overviewSummaryText.Text = "This will replace the current Start Menu pinned apps layout for $($scopeInfo.SummaryText) with the selected backup."
$backupFileText.Text = Split-Path -Path $BackupFilePath -Leaf
@@ -197,6 +217,10 @@ function Show-RestoreBackupDialog {
$primaryActionBtn.Visibility = 'Visible'
$primaryActionBtn.IsDefault = $true
$chooseRegistryBtn.IsDefault = $false
# Show intro panel so user can configure scope & auto-detect
$startMenuAutoBackupCheck.IsChecked = $true
$state.SelectedStartMenuBackupFilePath = $null
& $refreshStartMenuUi
}
@@ -252,7 +276,7 @@ function Show-RestoreBackupDialog {
$backupFileText.Text = Split-Path $SelectedBackupFilePath -Leaf
$backupCreatedText.Text = $createdText
$backupTargetText.Text = GetFriendlyRegistryBackupTarget -Target ([string]$SelectedBackup.Target)
$backupTargetText.Text = Get-FriendlyRegistryBackupTarget -Target ([string]$SelectedBackup.Target)
$featuresItemsControl.ItemsSource = $revertibleFeaturesList
$overviewFeaturesSection.Visibility = if ($revertibleFeaturesList.Count -gt 0) { 'Visible' } else { 'Collapsed' }
$reappliedFeaturesItemsControl.ItemsSource = $reappliedFeaturesList
@@ -292,9 +316,16 @@ function Show-RestoreBackupDialog {
}
Write-Host "Backup file selected: $($openDialog.FileName)"
$selectedBackup = Load-RegistryBackupFromFile -FilePath $openDialog.FileName
if (-not (& $showRegistryOverview -SelectedBackup $selectedBackup -SelectedBackupFilePath $openDialog.FileName)) {
try {
$selectedBackup = Import-RegistryBackup -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
}
@@ -324,31 +355,18 @@ function Show-RestoreBackupDialog {
}
if (-not $useManualBackupFile) {
$autoBackupExists = $false
if ($scope -eq 'AllUsers') {
$userPathString = GetUserDirectory -userName "*" -fileName "AppData\Local\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState"
$usersStartMenuPaths = Get-ChildItem -Path $userPathString -ErrorAction SilentlyContinue
foreach ($startMenuPath in $usersStartMenuPaths) {
if (Test-Path -LiteralPath (Join-Path $startMenuPath.FullName 'start2.bin.bak')) {
$autoBackupExists = $true
break
}
}
}
else {
$autoBackupPath = "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState\start2.bin.bak"
$autoBackupExists = Test-Path -LiteralPath $autoBackupPath
}
if (-not $autoBackupExists) {
$scopeText = (& $getStartMenuScopeInfo).SummaryText
Show-MessageBox -Owner $window -Title 'No Backup Found' -Message "No Start Menu backup file was found. You can uncheck the 'Automatically find Start Menu backup' option to select a backup file manually." -Button 'OK' -Icon 'Warning' | Out-Null
$scopeInfo = & $getStartMenuScopeInfo
$autoBackupPath = Get-StartMenuBackupPath -Scope $scopeInfo.Scope
if ($null -eq $autoBackupPath) {
$scopeText = $scopeInfo.SummaryText
Show-MessageBox -Owner $window -Title 'No Backup Found' -Message "No Start Menu backup file was found for $scopeText. Uncheck 'Automatically find Start Menu backup' to select a backup file manually." -Button 'OK' -Icon 'Warning' | Out-Null
return
}
$state.SelectedStartMenuBackupFilePath = if ($scopeInfo.Scope -eq 'CurrentUser') { $autoBackupPath } else { $null }
}
$window.Tag = @{
Result = 'RestoreStartMenu'
Result = 'Restore-StartMenu'
StartMenuScope = $scope
UseManualBackupFile = $useManualBackupFile
BackupFilePath = $state.SelectedStartMenuBackupFilePath
@@ -378,6 +396,7 @@ function Show-RestoreBackupDialog {
})
$startMenuScopeCombo.Add_SelectionChanged({
$state.SelectedStartMenuBackupFilePath = $null
& $refreshStartMenuUi
})
+21 -8
View File
@@ -1,6 +1,9 @@
<#
.SYNOPSIS
Shows the backup-restore dialog and performs the selected restore.
#>
function Show-RestoreBackupWindow {
param(
[Parameter(Mandatory = $false)]
[System.Windows.Window]$Owner = $null
)
@@ -28,11 +31,18 @@ function Show-RestoreBackupWindow {
}
Write-Host "User confirmed registry restore for $($backup.Target)."
Restore-RegistryBackupState -Backup $backup
$restoreResult.RestoredRegistry = $true
$successMessage = 'Registry backup restored successfully. Some changes may require a restart to take effect.'
$restoreOpResult = Restore-RegistryBackupState -Backup $backup
if ($restoreOpResult -and $restoreOpResult.Result) {
$restoreResult.RestoredRegistry = $true
if ($script:Params.ContainsKey("WhatIf")) {
$successMessage = '[WhatIf] Registry backup would be restored (no changes made).'
}
else {
$successMessage = 'Registry backup restored successfully. Some changes may require a restart to take effect.'
}
}
}
elseif ($dialogResult.Result -eq 'RestoreStartMenu') {
elseif ($dialogResult.Result -eq 'Restore-StartMenu') {
$scope = $dialogResult.StartMenuScope
$useManualBackupFile = ($dialogResult.UseManualBackupFile -eq $true)
$backupFilePath = $null
@@ -48,10 +58,10 @@ function Show-RestoreBackupWindow {
}
$result = if ($scope -eq 'AllUsers') {
RestoreStartMenuForAllUsers -BackupFilePath $backupFilePath
Restore-StartMenuForAllUsers -BackupFilePath $backupFilePath
}
else {
RestoreStartMenu -BackupFilePath $backupFilePath
Restore-StartMenu -BackupFilePath $backupFilePath
}
$resultEntries = @($result)
@@ -68,7 +78,10 @@ function Show-RestoreBackupWindow {
$warningMessage = "The Start Menu backup was successfully restored for $successCount user(s).`nSome users could not be restored:`n$failureSummary"
}
else {
if ($scope -eq 'AllUsers') {
if ($script:Params.ContainsKey("WhatIf")) {
$successMessage = '[WhatIf] Start Menu backup would be restored (no changes made).'
}
elseif ($scope -eq 'AllUsers') {
$successMessage = "The Start Menu backup was successfully restored for all users. The changes will apply the next time users sign in."
}
else {
-234
View File
@@ -1,234 +0,0 @@
param (
[switch]$CLI,
[switch]$Silent,
[switch]$Verbose,
[switch]$Sysprep,
[string]$LogPath,
[string]$User,
[switch]$NoRestartExplorer,
[switch]$CreateRestorePoint,
[switch]$RunAppsListGenerator,
[switch]$RunDefaults,
[switch]$RunDefaultsLite,
[switch]$RunSavedSettings,
[string]$Config,
[string]$Apps,
[string]$AppRemovalTarget,
[switch]$RemoveApps,
[switch]$RemoveAppsCustom,
[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 = @(
'CustomAppsList',
'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 ""
+41 -25
View File
@@ -1,13 +1,16 @@
param (
[switch]$Verbose,
[switch]$WhatIf,
[switch]$Dev,
[switch]$CLI,
[switch]$Silent,
[switch]$Verbose,
[switch]$Sysprep,
[string]$LogPath,
[string]$User,
[switch]$NoRestartExplorer,
[Alias('NoRestartExplorer')]
[switch]$SkipExplorerRestart,
[switch]$CreateRestorePoint,
[switch]$RunAppsListGenerator,
[switch]$SkipRegistryBackup,
[switch]$RunDefaults,
[switch]$RunDefaultsLite,
[switch]$RunSavedSettings,
@@ -15,7 +18,6 @@ param (
[string]$Apps,
[string]$AppRemovalTarget,
[switch]$RemoveApps,
[switch]$RemoveAppsCustom,
[switch]$RemoveGamingApps,
[switch]$RemoveHPApps,
[switch]$ForceRemoveEdge,
@@ -28,10 +30,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,
@@ -101,43 +105,46 @@ param (
[switch]$HideDriveLetters
)
# Show error if current powershell environment does not have LanguageMode set to FullLanguage
# Check if current PowerShell environment is limited by security policies
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
Write-Error "Win11Debloat is unable to run on your system, PowerShell execution is restricted by security policies"
Write-Output "Press any key to exit..."
$null = [System.Console]::ReadKey()
Exit 1
}
Clear-Host
Write-Output "-------------------------------------------------------------------------------------------"
Write-Output " Win11Debloat Script - Get"
Write-Output " Win11Debloat Script"
Write-Output "-------------------------------------------------------------------------------------------"
$tempRootPath = $env:TEMP
$tempWorkPath = Join-Path $tempRootPath 'Win11Debloat'
$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) {
Write-Output "> Downloading development version of Win11Debloat..."
$sourceUri = "https://github.com/Raphire/Win11Debloat/archive/refs/heads/master.zip"
} else {
Write-Output "> Downloading Win11Debloat..."
$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
Exit
Exit 1
}
# 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
}
@@ -153,7 +160,6 @@ if (Test-Path "$configDir") {
New-Item -ItemType Directory -Path "$backupDir" -Force | Out-Null
$filesToKeep = @(
'CustomAppsList',
'LastUsedSettings.json'
)
@@ -187,8 +193,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)"
}
@@ -200,7 +206,7 @@ $arguments = $($PSBoundParameters.GetEnumerator() | ForEach-Object {
Write-Output ""
Write-Output "> Launching Win11Debloat..."
# Minimize the powershell window when no parameters are provided
# Minimize the PowerShell window when no parameters are provided
if ($arguments.Count -eq 0) {
$windowStyle = "Minimized"
}
@@ -208,7 +214,7 @@ else {
$windowStyle = "Normal"
}
# Remove Powershell 7 modules from path to prevent module loading issues in the script
# 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 ';'
@@ -216,11 +222,20 @@ if ($PSVersionTable.PSVersion.Major -ge 7) {
# 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
$exitCode = 0
$debloatProcess = $null
try {
$debloatProcess = Start-Process powershell.exe -WindowStyle $windowStyle -PassThru -ArgumentList "-executionpolicy bypass -File `"$debloatScriptPath`" $arguments" -Verb RunAs -ErrorAction Stop
}
catch {
$exitCode = 1
Write-Error "Failed to start Win11Debloat: $_"
}
# Wait for the process to finish before continuing
if ($null -ne $debloatProcess) {
$debloatProcess.WaitForExit()
$exitCode = $debloatProcess.ExitCode
}
# Remove all remaining script files, except for configs, logs and backups
@@ -233,3 +248,4 @@ if (Test-Path $tempWorkPath) {
}
Write-Output ""
Exit $exitCode

Some files were not shown because too many files have changed in this diff Show More