Compare commits

..
25 Commits
Author SHA1 Message Date
Kobi HikriandGitHub 5f37ef4a80 ci: read the PR body from env instead of interpolating it into the shell (#4880) 2026-07-28 14:11:17 -05:00
OmarandGitHub d790118546 Add DNS-over-HTTPS (DoH) support (#4856)
* feat: Add DoH templates for DNS providers in configuration

* feat: Implement DoH support for DNS configuration and management

* feat(pester): enhance DNS tests to include DoH server address handling and template application

* fix: address codex feedback

* fix: update existing DoH server entries

* fix: enable DoH auto-upgrade

* fix: handle DNS setup failures

* fix: remove DoH state on DHCP reset
2026-07-28 14:09:44 -05:00
OmarGitHubChris Tituscoderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
5ebb24b107 Refactor Win11 Creator ISO workflow (#4862)
* refactor: simplify Win11 Creator ISO workflow

Keep no-driver ISO generation on the fast copy-only path.

Inject exported drivers with one selected-index WIM mount, one Add-Driver pass, and one commit.

Limit WinPE staging to boot-storage drivers and improve workflow validation coverage.

* refactor: stage Win11 setup script fallback

Copy the prepared setup script payloads into sources/$//Setup/Scripts so setup media does not depend solely on answer-file extension extraction.

Keep the existing autounattend execution path unchanged and cover the fallback files in the Win11 Creator tests.

* refactor: address codex feedback

* refactor: run ISO verification in runspace

Rename the ISO logger to Write-WinUtilISOLog so the shared runspace pool imports it automatically. Run Mount & Verify off the WPF thread with dispatcher-safe UI updates, and add Pester coverage for the runspace and logger contract.

* refactor: address ISO review feedback

Keep the selected ISO stable during background verification. Apply ContentDeliveryManager settings to both the first account and the default profile. Block FAT32 USB creation when install.esd exceeds the supported file size.

* refactor: address ISO setup edge cases

Set BypassNRO before OOBE so local account setup is available during Windows Setup.

Detect registered DISM mounts during driver-injection cleanup so partial mount failures are discarded.

* refactor: address ISO review edge cases

Use the actual FAT32 file limit for install.esd USB checks.

Disable ISO modification while mount verification is running.

Apply unsupported-hardware notice suppression to the first user profile.

* refactor: stage ISO setup safeguards earlier

Set device encryption and reserved-storage registry guards before OOBE.

Enable the OEM configuration-set fallback when setup scripts are staged there.

* Update functions/private/Invoke-WinUtilISOUSB.ps1

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

---------

Co-authored-by: Chris Titus <contact@christitus.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-07-28 14:08:56 -05:00
Chris TitusandGitHub fae58d73c4 Update sponsors in README (#4881) 2026-07-28 11:10:13 -05:00
Chris TitusandGitHub 9e2989964e Update sponsors in README (#4878) 2026-07-27 11:14:18 -05:00
OmarandGitHub 50be7390e5 fix: restore WindowStyle to SingleBorderWindow for proper window framing (#4868) 2026-07-22 12:42:25 -05:00
Chris TitusandGitHub 8ab02ae756 chore: Update generated dev docs (#4873) 2026-07-22 12:42:13 -05:00
OmarandGitHub 7d1a03dcc9 bugfix(tweaks): fix OneDrive uninstall path (#4864)
* bugfix(tweaks): fix OneDrive uninstall path

* bugfix(tweaks): use SystemRoot for OneDrive path
2026-07-22 12:41:48 -05:00
Paweł GwozdowskiandGitHub 5a4c9c883d Revert "Rework contributing.md (#4567)" (#4855) 2026-07-22 12:34:14 -05:00
Chris Titus 5c104d02e2 Update automated update PR workflows and filter release notes 2026-07-16 13:39:33 -05:00
Chris TitusandGitHub 06f33e88ba Fix Show Installed Apps reliability and selection (#4850)
* Fix Windows PowerShell runspace cleanup callback

* Fix installed app package ID matching

* Synchronize installed app selection UI

* Streamline installed app detection

* Avoid installed app UI runspace deadlock
2026-07-16 13:30:12 -05:00
b9dee86694 refactor: style and align Install tab filter chips (#4848)
* refactor: implement FilterChipStyle for Install tab category filters

- Created a dedicated FilterChipStyle style block with custom padding, cursor, and height definitions

- Applied FilterChipStyle to all category filter buttons

- Added a new 'Filters' TextBlock section title aligned with the sidebar actions

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Chris Titus <contact@christitus.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-16 10:37:25 -05:00
OmarandGitHub 3d1904ebbe fix: wrap Tweaks header buttons to prevent layout clipping (#4847) 2026-07-16 10:29:16 -05:00
Chris TitusandGitHub 70f1c50bdd chore: Update generated dev docs (#4846) 2026-07-15 23:37:16 -05:00
2e5df82979 Update sponsors in README (#4820)
Co-authored-by: ChrisTitusTech <7896101+ChrisTitusTech@users.noreply.github.com>
2026-07-15 23:37:06 -05:00
Chris TitusandGitHub 29595f90e2 Fix progress, update profiles, and documentation links (#4845)
* Improve O&O ShutUp10++ download flow

* Refine Windows Update profile behavior and docs

* Fix documentation link regressions

* Restore updates when applying recommended profile
2026-07-15 23:36:46 -05:00
Chris TitusandGitHub 9ed06e793a Replace install progress overlay with shared indicator (#4844)
* Use shared progress for app workflows

* Remove legacy install progress overlay

* Document shared app progress indicator

* Address progress review feedback

* Track all Win11 ISO progress

* Restore Win11 Creator work on relaunch
2026-07-15 22:35:58 -05:00
51cb3811dd Add category search chips to Install tab for improved filtering (#4826)
* Add Quick Category Search Chips to Install tab

Add category shortcut chips on the Install tab that insert the category
name into the existing search box. Append each app's category as a
description tag so the existing description search filters by category.
Reuses the standard button style and existing search; no search, install,
or preset logic is changed.

* Fix install category chip filtering

* Fix category filter refresh behavior

---------

Co-authored-by: Chris Titus <contact@christitus.com>
2026-07-15 20:54:22 -05:00
975616b58f Fix search bar overflowing behind icon buttons at narrow window sizes (#4831)
* fix(search): make search bar stretch instead of overflowing at narrow window sizes

* fix(ui): sync runtime MinWidth override with XAML value

Add_Loaded was hardcoding $sync.Form.MinWidth back to 1000 after
load, overriding the 1150 set in inputXML.xaml and letting the
window shrink below the space the search bar/toolbar layout needs.

* fix(ui): address responsive search review feedback

* fix(ui): enforce usable toolbar width

---------

Co-authored-by: Chris Titus <contact@christitus.com>
2026-07-15 20:41:41 -05:00
11783c75a8 fix(dns): scope leaked ToggleButton style and theme the ComboBox dropdown popup (#4835)
Co-authored-by: Chris Titus <contact@christitus.com>
2026-07-15 17:09:10 -05:00
13a18eeeb2 Add maximaze button (#4841)
* Add maximize button logic and update assets

* Add maximize button with theming

* delelte logo

* Center and restyle the window control buttons

* Fix window state test setup

---------

Co-authored-by: aran628 <aran2014+@gmail.com>
Co-authored-by: Chris Titus <contact@christitus.com>
2026-07-15 16:59:51 -05:00
Chris TitusandGitHub 822003d87f Fix AppX detection and add package restore support (#4842)
* Fix installed AppX package detection

* Add AppX package restore workflow

* Add AppX operation progress feedback

* Fix AppX operation launch race

* Handle AppX workflow failures
2026-07-15 15:57:56 -05:00
c8c3c7869f Add window-level progress indicator for Run Tweaks and Undo (#4830)
* feat(tweaks): add window-level progress indicator for Run Tweaks and Undo

* Hide completed tweaks progress on next action

---------

Co-authored-by: Chris Titus <contact@christitus.com>
2026-07-15 14:17:42 -05:00
Paweł GwozdowskiandGitHub 157a66b14d Update Invoke-WPFUIElements.ps1 (#4824) 2026-07-13 13:20:53 -05:00
9791386f53 fix: resolve silent failures in AppX removal and PS7 DISM bugs (#4822)
* fix: resolve silent failures in AppX removal and PS7 DISM bugs

- Fixed a bug where Remove-AppxPackage failed silently in PowerShell 7 due to pipeline binding issues. We now explicitly loop through Get-AppxPackage results and pass PackageFullName directly.
- Added wildcard matching to Get-AppxPackage and Get-AppxProvisionedPackage to reliably locate packages regardless of short name vs family name variations.
- Offloaded Get-AppxProvisionedPackage and Remove-AppxProvisionedPackage execution to Windows PowerShell 5.1 (powershell.exe) to bypass PowerShell 7 DISM COM exceptions ("Class not registered") and performance hangs.
- Deduplicated AppX removal logic by calling Remove-WinUtilAPPX directly from Invoke-WPFAppxRemoval instead of copying the implementation.

* refactor(Remove-WinUtilAPPX): use script block to improve readability

* feat: enhance AppX removal process and add provisioned package removal function

* fix(tests): update AppX removal tests

This also adds tests for the new Remove-WinUtilProvisionedAPPX functionality

* Move AppX removal under Tweaks

* Address AppX removal review feedback

* Complete AppX removal error handling

* Prevent AppX cleanup confirmation prompts

---------

Co-authored-by: Chris Titus <contact@christitus.com>
2026-07-13 13:16:35 -05:00
78 changed files with 3989 additions and 1630 deletions
+88 -114
View File
@@ -1,149 +1,123 @@
## Contributing Code
# How to Contribute?
### Before You Start
## Testing
- Keep each pull request focused on a single feature or fix.
- Avoid unnecessary formatting changes or large unrelated edits.
- Document what changed and why in your PR description.
* Test the latest changes to WinUtil by running the pre-release and reporting issues you are encountering to help us continually improve WinUtil!
---
#### **Run the latest pre-release**
```ps1
irm https://christitus.com/windev | iex
```
## Basic Git Workflow
!!! bug "Keep in mind"
### 1. Fork the Repository
This is a pre-release and should be treated as such. It exists for developers to test the utility and report or fix bugs before they get added to the stable release. Don't use it in production!
Go to the ChrisTitusTech/winutil repository on GitHub and click the Fork button in the top right corner.
## Issues
<img width="171" height="50" alt="{650A4723-F38A-44A4-9820-D232BC87C8A0}" src="https://github.com/user-attachments/assets/a214f27c-2fee-444a-920f-d87b14f5896f" />
* If you encounter any challenges or problems with the script, I kindly request that you submit them via the "Issues" tab on the GitHub repository. By filling out the provided template, you can provide specific details about the issue, allowing me (and others in the community) to promptly address any bugs or consider feature requests.
---
## Contribute Code
### 2. Clone Your Fork
* Pull requests are now handled directly on the **MAIN branch**. This was done since we can now select specific releases to launch via releases in GitHub.
```powershell
git clone https://github.com/YOUR_USERNAME/winutil.git
cd winutil
* If you're doing code changes, then you can submit a PR to the `main` branch.
!!! warning "Important"
Do not use a code formatter, make massive amounts of line changes, or make multiple feature changes. EACH FEATURE CHANGE SHOULD BE IT'S OWN PULL REQUEST!
Do not open a pull request that adds support for other languages to WinUtil for now, until we decide how we want to move forward with language support.
* When creating pull requests, it is essential to thoroughly document all changes made. This includes, but is not limited to, documenting any additions made to the `tweaks` section and corresponding `undo tweak`, so users are able to remove the newly added tweaks if necessary, and comprehensive documentation is required for all code changes. Document your changes and briefly explain why you made your changes in your Pull Request Description. Failure to adhere to this format may result in the denial of the pull request. Additionally, any code lacking sufficient documentation may also be denied.
* By following these guidelines, we can maintain a high standard of quality and ensure that the codebase remains organized and well-documented.
!!! note
When creating a function, please include "WPF" or "WinUtil" in the file name so it can be loaded into the runspace.
## Walk through
* This is a guide for beginners. If you are still having issues, look at the following official GitHub documentation:
* [Commit through WEB](https://docs.github.com/en/pull-requests/committing-changes-to-your-project/creating-and-editing-commits/about-commits)
* [Commit through GitHub Desktop](https://docs.github.com/en/desktop/making-changes-in-a-branch/committing-and-reviewing-changes-to-your-project-in-github-desktop#about-commits)
* [Create a Pull Request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request)
### Overview
``` mermaid
%%{init: {"flowchart": {"curve": "cardinal"}} }%%
graph TD
  A[Fork Project] --> B[Clone Repository];
  B --> C[Create New Branch];
  C --> D[Make Changes];
  D --> G[Test Changes];
  G --> H{Tests Passed?};
  H -->|Yes| E[Commit Changes];
  H -->|No| J[Fix Issues];
  J --> G;
  E --> F[Push Branch];
  F --> K[Create Pull Request];
  K --> L[Fill out PR template];
classDef default stroke:#333,stroke-width:4px,font-size:12pt;
```
!!! info
---
This is a diagram to guide you through the process. It may vary depending on the type of change you're making.
### 3. Create a Branch
### Fork the Repo
* Fork the WinUtil Repository [here](https://github.com/ChrisTitusTech/winutil) to create a copy that will be available in your repository list.
Never work directly on `main`.
![Fork Image](/docs/assets/images/Fork-Button-Dark.png#gh-dark-mode-only)
Create a branch related to your change:
![Fork Image](/docs/assets/images/Fork-Button-Light.png#only-light#gh-light-mode-only)
```powershell
git checkout -b feature-name
```
### Clone the Fork
!!! tip
Example:
While you can make your changes directly through the Web, we recommend cloning the repo to your device using the application GitHub Desktop (available in WinUtil) to test your fork easily.
```powershell
git checkout -b add-firefox-tweak
```
* Install GitHub Desktop if it is not already installed.
* Log in using the same GitHub account you used to fork WinUtil.
* Choose the fork under "Your Repositories" and press "clone {repo name}"
* Create a new branch and name it something relatable to your changes.
---
* Now you can modify WinUtil to your liking using your preferred text editor.
### 4. Edit the Code
Open the project in your preferred text editor and make your changes.
### Testing your changes
Keep changes small and focused.
* To test to see if your changes work as intended, run the following commands in a PowerShell terminal as admin:
---
* Change the directory where you are running the commands to the forked project.
* `cd {path to the folder with the compile.ps1}`
* Run the following command to compile and run WinUtil:
* `.\Compile.ps1 -run`
### 5. Test Your Changes
![Compile](/docs/assets/images/Complie.png)
Open Powershell as Administrator.
Go to the project folder:
Open PowerShell as Administrator.
```powershell
cd path\to\winutil
```
* After seeing that your changes work properly, feel free to commit the changes to the repository and make a PR. For help on that, follow the documentation below.
Run:
```powershell
.\Compile.ps1 -Run
```
### Committing the changes
* Before committing your changes, please discard changes made to the `winutil.ps1` file, like the following:
Verify:
![Push Commit Image](/docs/assets/images/Discard-GHD.png)
- WinUtil launches correctly
- Your feature works
- Nothing else breaks
* Now, commit your changes once you are happy with the result.
If something fails, fix it before committing.
![Commit Image](/docs/assets/images/Commit-GHD.png)
---
* Push the changes to upload them to your fork on github.com.
### 6. Review Your Changes
![Push Commit Image](/docs/assets/images/Push-Commit.png)
Check what changed:
```powershell
git status
```
Review the diff:
```powershell
git diff
```
Make sure you did not accidentally modify unrelated files.
---
### 7. Commit Your Changes
Stage files:
```powershell
git add .
```
Commit them:
```powershell
git commit -m "Add feature description"
```
Example:
```powershell
git commit -m "Add Firefox package tweak"
```
---
### 8. Push Your Branch
```powershell
git push origin branch-name
```
Example:
```powershell
git push origin add-firefox-tweak
```
---
### 9. Open a Pull Request
Go to your fork on GitHub.
GitHub will show a button to create a pull request.
<img width="1009" height="71" alt="{C8C6A3CC-79D4-44FD-A54C-4C5717F12730}" src="https://github.com/user-attachments/assets/0419d193-d4e7-47c0-87cf-b986742201a0" />
Before submitting:
- Explain what changed
- Explain why you changed it
- Make sure unrelated files are not included
Once submitted, maintainers will review your PR.
### Making a PR
* To make a PR on your repo under a new branch linking to the main branch, a button will show and say Preview and Create pull request. Click that button and fill in all the information that is provided on the template. Once all the information is filled in correctly, check your PR to make sure there is no WinUtil.ps1 file attached to the PR. Once everything is good, make the PR and wait for Chris (the maintainer) to accept or deny your PR. Once it is accepted by Chris, you will be able to see your changes in the "/windev" build.
* If you do not see your feature in the main "/win" build, that is fine. All new changes go into the /windev build to make sure everything is working OK before going fully public.
* Congratulations! You just submitted your first PR. Thank you so much for contributing to WinUtil.
+2 -2
View File
@@ -1,4 +1,4 @@
name: Auto-merge Docs PRs
name: Auto-merge Automated Update PRs
on:
pull_request:
@@ -8,7 +8,7 @@ on:
jobs:
auto-merge:
if: github.event.pull_request.head.ref == 'docs-update' && (github.event.pull_request.user.login == 'ChrisTitusTech' || github.event.pull_request.user.login == 'github-actions[bot]')
if: (github.event.pull_request.head.ref == 'docs-update' || github.event.pull_request.head.ref == 'sponsors-update') && (github.event.pull_request.user.login == 'ChrisTitusTech' || github.event.pull_request.user.login == 'github-actions[bot]')
runs-on: ubuntu-latest
permissions:
pull-requests: write
@@ -18,10 +18,11 @@ jobs:
- name: Extract Discussion Number & Close If any Were Found
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_BODY: ${{ github.event.pull_request.body }}
if: github.event.pull_request.merged == true
id: extract-discussion
run: |
pr_body="${{ github.event.pull_request.body }}"
pr_body="$PR_BODY"
discussion_ids=$(echo "$pr_body" | grep -oP '(?i)(resolve|fix|close)[s|d]? #\K[0-9]+')
if [ -z "$discussion_ids" ]; then
+16 -1
View File
@@ -109,6 +109,21 @@ jobs:
config-name: release-drafter.yml
version: ${{ env.VERSION }}
- name: Remove Automated Updates from Release Notes
id: filter_notes
shell: pwsh
env:
RELEASE_NOTES: ${{ steps.generate_notes.outputs.body }}
run: |
$filteredNotes = ($env:RELEASE_NOTES -split '\r?\n' | Where-Object {
$_ -notmatch '(?i)Update (Generated Dev Docs|Sponsors)'
}) -join "`n"
$delimiter = [guid]::NewGuid().ToString()
"body<<$delimiter" >> $env:GITHUB_OUTPUT
$filteredNotes >> $env:GITHUB_OUTPUT
$delimiter >> $env:GITHUB_OUTPUT
- name: Create and Upload Release
id: create_release
uses: softprops/action-gh-release@v3
@@ -116,7 +131,7 @@ jobs:
tag_name: ${{ env.VERSION }}
name: Pre-Release ${{ env.VERSION }}
body: |
${{ steps.generate_notes.outputs.body }}
${{ steps.filter_notes.outputs.body }}
![GitHub Downloads (specific asset, specific tag)](https://img.shields.io/github/downloads/ChrisTitusTech/winutil/${{ env.VERSION }}/winutil.ps1)
append_body: false
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
id: cpr
uses: peter-evans/create-pull-request@v8
with:
token: ${{ secrets.GITHUB_TOKEN }}
token: ${{ secrets.AUTO_MERGE }}
commit-message: 'Update sponsors in README'
title: 'chore: Update Sponsors README'
body: 'Automated update of sponsors section'
+1
View File
@@ -189,5 +189,6 @@ When the user corrects an agent approach, add or tighten one concrete rule here
- When the active log file is owned by `Start-Transcript`, do not call `Add-Content` against that file; write to host output so the transcript captures the line in the same log file without recording a terminating-error diagnostic.
- Log install/uninstall package names and package-manager IDs before queuing background runspace work; do not rely on runspace host output for the package identity.
- For Win11 Creator, start each new ISO modification in a fresh `WinUtil_Win11ISO_*` temp directory; existing-work detection is only for resuming/exporting already modified media.
- For Win11 Creator driver injection, keep offline WIM servicing to one mount, one `/Add-Driver`, and one commit; do not export editions or run unrelated WIM cleanup, and reject damaged metadata before ISO export.
- For Script Analyzer cleanup, fix actionable source warnings first and do not globally suppress accepted convention warnings such as plural names, `ShouldProcess` on UI helpers, `$global:sync`, or compile-time cross-file false positives.
- For DNS DHCP reset, keep the cmdlet reset and explicitly set IPv4 and IPv6 DNS source to DHCP.
+1 -1
View File
@@ -77,7 +77,7 @@ See https://github.com/ChrisTitusTech/winutil/blob/main/.github/CONTRIBUTING.md
These are the sponsors that help keep this project alive with monthly contributions.
<!-- sponsors --><a href="https://github.com/ysaito8015"><img src="https:&#x2F;&#x2F;github.com&#x2F;ysaito8015.png" width="60px" alt="User avatar: Yusuke Saito" /></a><a href="https://github.com/dwelfusius"><img src="https:&#x2F;&#x2F;github.com&#x2F;dwelfusius.png" width="60px" alt="User avatar: " /></a><a href="https://github.com/mews-se"><img src="https:&#x2F;&#x2F;github.com&#x2F;mews-se.png" width="60px" alt="User avatar: Martin Stockzell" /></a><a href="https://github.com/jdiegmueller"><img src="https:&#x2F;&#x2F;github.com&#x2F;jdiegmueller.png" width="60px" alt="User avatar: Jason A. Diegmueller" /></a><a href="https://github.com/robertsandrock"><img src="https:&#x2F;&#x2F;github.com&#x2F;robertsandrock.png" width="60px" alt="User avatar: RMS" /></a><a href="https://github.com/paulsheets"><img src="https:&#x2F;&#x2F;github.com&#x2F;paulsheets.png" width="60px" alt="User avatar: Paul" /></a><a href="https://github.com/djones369"><img src="https:&#x2F;&#x2F;github.com&#x2F;djones369.png" width="60px" alt="User avatar: Dave J (WhamGeek)" /></a><a href="https://github.com/anthonymendez"><img src="https:&#x2F;&#x2F;github.com&#x2F;anthonymendez.png" width="60px" alt="User avatar: Anthony Mendez" /></a><a href="https://github.com/FatBastard0"><img src="https:&#x2F;&#x2F;github.com&#x2F;FatBastard0.png" width="60px" alt="User avatar: " /></a><a href="https://github.com/DursleyGuy"><img src="https:&#x2F;&#x2F;github.com&#x2F;DursleyGuy.png" width="60px" alt="User avatar: DursleyGuy" /></a><a href="https://github.com/DwayneTheRockLobster1"><img src="https:&#x2F;&#x2F;github.com&#x2F;DwayneTheRockLobster1.png" width="60px" alt="User avatar: " /></a><a href="https://github.com/KieraKujisawa"><img src="https:&#x2F;&#x2F;github.com&#x2F;KieraKujisawa.png" width="60px" alt="User avatar: Kiera Meredith" /></a><a href="https://github.com/andrewpayne68"><img src="https:&#x2F;&#x2F;github.com&#x2F;andrewpayne68.png" width="60px" alt="User avatar: Andrew P" /></a><a href="https://github.com/johanwildeboer"><img src="https:&#x2F;&#x2F;github.com&#x2F;johanwildeboer.png" width="60px" alt="User avatar: " /></a><a href="https://github.com/lukas346"><img src="https:&#x2F;&#x2F;github.com&#x2F;lukas346.png" width="60px" alt="User avatar: Wook" /></a><a href="https://github.com/tsv31"><img src="https:&#x2F;&#x2F;github.com&#x2F;tsv31.png" width="60px" alt="User avatar: Sorin" /></a><!-- sponsors -->
<!-- sponsors --><a href="https://github.com/ysaito8015"><img src="https:&#x2F;&#x2F;github.com&#x2F;ysaito8015.png" width="60px" alt="User avatar: Yusuke Saito" /></a><a href="https://github.com/dwelfusius"><img src="https:&#x2F;&#x2F;github.com&#x2F;dwelfusius.png" width="60px" alt="User avatar: " /></a><a href="https://github.com/mews-se"><img src="https:&#x2F;&#x2F;github.com&#x2F;mews-se.png" width="60px" alt="User avatar: Martin Stockzell" /></a><a href="https://github.com/jdiegmueller"><img src="https:&#x2F;&#x2F;github.com&#x2F;jdiegmueller.png" width="60px" alt="User avatar: Jason A. Diegmueller" /></a><a href="https://github.com/robertsandrock"><img src="https:&#x2F;&#x2F;github.com&#x2F;robertsandrock.png" width="60px" alt="User avatar: RMS" /></a><a href="https://github.com/paulsheets"><img src="https:&#x2F;&#x2F;github.com&#x2F;paulsheets.png" width="60px" alt="User avatar: Paul" /></a><a href="https://github.com/djones369"><img src="https:&#x2F;&#x2F;github.com&#x2F;djones369.png" width="60px" alt="User avatar: Dave J (WhamGeek)" /></a><a href="https://github.com/anthonymendez"><img src="https:&#x2F;&#x2F;github.com&#x2F;anthonymendez.png" width="60px" alt="User avatar: Anthony Mendez" /></a><a href="https://github.com/FatBastard0"><img src="https:&#x2F;&#x2F;github.com&#x2F;FatBastard0.png" width="60px" alt="User avatar: " /></a><a href="https://github.com/DursleyGuy"><img src="https:&#x2F;&#x2F;github.com&#x2F;DursleyGuy.png" width="60px" alt="User avatar: DursleyGuy" /></a><a href="https://github.com/DwayneTheRockLobster1"><img src="https:&#x2F;&#x2F;github.com&#x2F;DwayneTheRockLobster1.png" width="60px" alt="User avatar: " /></a><a href="https://github.com/KieraKujisawa"><img src="https:&#x2F;&#x2F;github.com&#x2F;KieraKujisawa.png" width="60px" alt="User avatar: Kiera Meredith" /></a><a href="https://github.com/andrewpayne68"><img src="https:&#x2F;&#x2F;github.com&#x2F;andrewpayne68.png" width="60px" alt="User avatar: Andrew P" /></a><a href="https://github.com/seanh1995"><img src="https:&#x2F;&#x2F;github.com&#x2F;seanh1995.png" width="60px" alt="User avatar: Sean (ANGRYxScotsman)" /></a><a href="https://github.com/Abs313a"><img src="https:&#x2F;&#x2F;github.com&#x2F;Abs313a.png" width="60px" alt="User avatar: Abs" /></a><!-- sponsors -->
---
+60 -30
View File
@@ -4,203 +4,232 @@
"Content": "Feedback Hub",
"Description": "Allows users to submit bug reports, feature suggestions, and diagnostic data directly to Microsoft.",
"Panel": "0",
"PackageId": "Microsoft.WindowsFeedbackHub"
"PackageId": "Microsoft.WindowsFeedbackHub",
"StoreId": "9NBLGGH4R32N"
},
"WPFAppxMicrosoft_GetHelp": {
"Category": "Microsoft Apps",
"Content": "Get Help",
"Description": "Provides access to automated troubleshooting guides, support documentation, and direct Microsoft customer assistance.",
"Panel": "0",
"PackageId": "Microsoft.GetHelp"
"PackageId": "Microsoft.GetHelp",
"StoreId": "9PKDZBMV1H3T"
},
"WPFAppxMicrosoft_OutlookForWindows": {
"Category": "Microsoft Apps",
"Content": "Outlook for Windows",
"Description": "Provides modern email management, calendar scheduling, and contact organization features.",
"Panel": "0",
"PackageId": "Microsoft.OutlookForWindows"
"PackageId": "Microsoft.OutlookForWindows",
"StoreId": "9NRX63209R7B"
},
"WPFAppxMSTeams": {
"Category": "Microsoft Apps",
"Content": "Microsoft Teams",
"Description": "Facilitates instant messaging, video conferencing, file sharing, and workspace collaboration.",
"Panel": "0",
"PackageId": "MSTeams"
"PackageId": "MSTeams",
"StoreId": "XP8BT8DW290MPQ"
},
"WPFAppxClipchamp_Clipchamp": {
"Category": "Utilities & Productivity",
"Content": "Clipchamp",
"Description": "Provides a user-friendly video editor with built-in templates, effects, and timeline editing tools.",
"Panel": "0",
"PackageId": "Clipchamp.Clipchamp"
"PackageId": "Clipchamp.Clipchamp",
"StoreId": "9P1J8S7CCWWT"
},
"WPFAppxMicrosoft_MicrosoftOfficeHub": {
"Category": "Microsoft Apps",
"Content": "Microsoft 365",
"Description": "Serves as a centralized launcher and dashboard for accessing cloud-based Microsoft 365 apps and recent documents.",
"Panel": "0",
"PackageId": "Microsoft.MicrosoftOfficeHub"
"PackageId": "Microsoft.MicrosoftOfficeHub",
"StoreId": "9WZDNCRD29V9"
},
"WPFAppxMicrosoft_ZuneMusic": {
"Category": "Utilities & Productivity",
"Content": "Media Player",
"Description": "Plays local audio and video files with modern playlist management and casting capabilities.",
"Panel": "0",
"PackageId": "Microsoft.ZuneMusic"
"PackageId": "Microsoft.ZuneMusic",
"StoreId": "9WZDNCRFJ3PT"
},
"WPFAppxMicrosoft_BingSearch": {
"Category": "Bing & Web Services",
"Content": "Bing Search",
"Description": "Integrates Microsoft Bing search capabilities and web services directly into the operating system.",
"Panel": "1",
"PackageId": "Microsoft.BingSearch"
"PackageId": "Microsoft.BingSearch",
"StoreId": "9NZBF4GT040C"
},
"WPFAppxMicrosoftCorporationII_QuickAssist": {
"Category": "Utilities & Productivity",
"Content": "Quick Assist",
"Description": "Enables secure remote technical support and screen sharing over an internet connection.",
"Panel": "0",
"PackageId": "MicrosoftCorporationII.QuickAssist"
"PackageId": "MicrosoftCorporationII.QuickAssist",
"StoreId": "9P7BP5VNWKX5"
},
"WPFAppxMicrosoft_WindowsDevHome": {
"Category": "Developer Tools",
"Content": "Dev Home",
"Description": "Provides a specialized dashboard for software developer environment setups, repository syncing, and hardware widgets.",
"Panel": "1",
"PackageId": "Microsoft.Windows.DevHome"
"PackageId": "Microsoft.Windows.DevHome",
"StoreId": "9N8MHTPHNGVV"
},
"WPFAppxMicrosoft_WindowsCrossDevice": {
"Category": "Microsoft Ecosystem",
"Content": "Mobile Devices",
"Description": "Manages system-level background connectivity with paired mobile devices. Removing this may disable cross-device features such as phone screen mirroring, file transfer, and mobile hotspot handoff integrated into Windows Settings.",
"Panel": "0",
"PackageId": "MicrosoftWindows.CrossDevice"
"PackageId": "MicrosoftWindows.CrossDevice",
"StoreId": "9NTXGKQ8P7N0"
},
"WPFAppxMicrosoft_Todos": {
"Category": "Utilities & Productivity",
"Content": "To Do",
"Description": "Creates, tracks, and synchronizes personal tasks, smart lists, and daily reminders.",
"Panel": "0",
"PackageId": "Microsoft.Todos"
"PackageId": "Microsoft.Todos",
"StoreId": "9NBLGGH5R558"
},
"WPFAppxMicrosoft_PowerAutomateDesktop": {
"Category": "Developer Tools",
"Content": "Power Automate",
"Description": "Automates repetitive workflows and desktop tasks using low-code visual scripting.",
"Panel": "1",
"PackageId": "Microsoft.PowerAutomateDesktop"
"PackageId": "Microsoft.PowerAutomateDesktop",
"StoreId": "9NFTCH6J7FHV"
},
"WPFAppxMicrosoft_YourPhone": {
"Category": "Microsoft Ecosystem",
"Content": "Phone Link",
"Description": "Synchronizes text messages, phone notifications, photos, and calls from a mobile device to the desktop.",
"Panel": "0",
"PackageId": "Microsoft.YourPhone"
"PackageId": "Microsoft.YourPhone",
"StoreId": "9NMPJ99VJBWV"
},
"WPFAppxMicrosoft_MicrosoftStickyNotes": {
"Category": "Utilities & Productivity",
"Content": "Sticky Notes",
"Description": "Creates quick, floating text notes on the desktop that automatically sync across devices.",
"Panel": "0",
"PackageId": "Microsoft.MicrosoftStickyNotes"
"PackageId": "Microsoft.MicrosoftStickyNotes",
"StoreId": "9NBLGGH4QGHW"
},
"WPFAppxMicrosoft_WindowsSoundRecorder": {
"Category": "Utilities & Productivity",
"Content": "Sound Recorder",
"Description": "Records and trims live audio inputs with simple microphone adjustment controls.",
"Panel": "0",
"PackageId": "Microsoft.WindowsSoundRecorder"
"PackageId": "Microsoft.WindowsSoundRecorder",
"StoreId": "9WZDNCRFHWKN"
},
"WPFAppxMicrosoft_WindowsAlarms": {
"Category": "Utilities & Productivity",
"Content": "Clock",
"Description": "Features world clocks, alarms, countdown timers, stopwatches, and dedicated focus session tracking.",
"Panel": "0",
"PackageId": "Microsoft.WindowsAlarms"
"PackageId": "Microsoft.WindowsAlarms",
"StoreId": "9WZDNCRFJ3PR"
},
"WPFAppxMicrosoft_Paint": {
"Category": "Utilities & Productivity",
"Content": "Paint",
"Description": "Provides built-in digital sketching, basic image editing, and pixel-level graphic manipulation tools.",
"Panel": "0",
"PackageId": "Microsoft.Paint"
"PackageId": "Microsoft.Paint",
"StoreId": "9PCFS5B6T72H"
},
"WPFAppxMicrosoft_WindowsNotepad": {
"Category": "Utilities & Productivity",
"Content": "Notepad",
"Description": "Provides a lightweight text editor with multi-tab support for plain text files and code snippets.",
"Panel": "0",
"PackageId": "Microsoft.WindowsNotepad"
"PackageId": "Microsoft.WindowsNotepad",
"StoreId": "9MSMLRH6LZF3"
},
"WPFAppxMicrosoft_ScreenSketch": {
"Category": "Utilities & Productivity",
"Content": "Snipping Tool",
"Description": "Captures screenshots or screen recordings with built-in markup, image cropping, and optical character recognition (OCR).",
"Panel": "0",
"PackageId": "Microsoft.ScreenSketch"
"PackageId": "Microsoft.ScreenSketch",
"StoreId": "9MZ95KL8MR0L"
},
"WPFAppxMicrosoft_Copilot": {
"Category": "Bing & Web Services",
"Content": "Copilot",
"Description": "Launches the Microsoft AI companion for contextual answers, creative writing assistance, and intelligent web search.",
"Panel": "1",
"PackageId": "Microsoft.Copilot"
"PackageId": "Microsoft.Copilot",
"StoreId": "9NHT9RB2F4HD"
},
"WPFAppxMicrosoft_WindowsCalculator": {
"Category": "Utilities & Productivity",
"Content": "Calculator",
"Description": "Performs standard arithmetic, scientific operations, programming calculations, and unit conversions.",
"Panel": "0",
"PackageId": "Microsoft.WindowsCalculator"
"PackageId": "Microsoft.WindowsCalculator",
"StoreId": "9WZDNCRFHVN5"
},
"WPFAppxMicrosoft_WindowsCamera": {
"Category": "Utilities & Productivity",
"Content": "Camera",
"Description": "Captures photographs and records video files via connected webcams or imaging hardware.",
"Panel": "0",
"PackageId": "Microsoft.WindowsCamera"
"PackageId": "Microsoft.WindowsCamera",
"StoreId": "9WZDNCRFJBBG"
},
"WPFAppxMicrosoft_WindowsPhotos": {
"Category": "Utilities & Productivity",
"Content": "Photos",
"Description": "Organizes, views, and crops local images with basic color adjustment and album creation tools.",
"Panel": "0",
"PackageId": "Microsoft.Windows.Photos"
"PackageId": "Microsoft.Windows.Photos",
"StoreId": "9WZDNCRFJBH4"
},
"WPFAppxMicrosoft_BingNews": {
"Category": "Bing & Web Services",
"Content": "News",
"Description": "Aggregates breaking news headlines, personalized article feeds, and world current events.",
"Panel": "1",
"PackageId": "Microsoft.BingNews"
"PackageId": "Microsoft.BingNews",
"StoreId": "9WZDNCRFHVFW"
},
"WPFAppxMicrosoft_BingWeather": {
"Category": "Bing & Web Services",
"Content": "Weather",
"Description": "Displays local real-time weather tracking, radar maps, and historical meteorological forecasts.",
"Panel": "1",
"PackageId": "Microsoft.BingWeather"
"PackageId": "Microsoft.BingWeather",
"StoreId": "9WZDNCRFJ3Q2"
},
"WPFAppxMicrosoft_GamingApp": {
"Category": "Xbox & Gaming",
"Content": "Xbox App",
"Description": "Serves as the primary gaming library manager, social community interface, and PC Game Pass dashboard.",
"Panel": "1",
"PackageId": "Microsoft.GamingApp"
"PackageId": "Microsoft.GamingApp",
"StoreId": "9MV0B5HZVK9Z"
},
"WPFAppxMicrosoft_XboxGamingOverlay": {
"Category": "Xbox & Gaming",
"Content": "Xbox Game Bar",
"Description": "Provides customizable in-game status widgets, audio balancing sliders, system monitoring tools, and gameplay recording.",
"Panel": "1",
"PackageId": "Microsoft.XboxGamingOverlay"
"PackageId": "Microsoft.XboxGamingOverlay",
"StoreId": "9NZKPSTSNW4P"
},
"WPFAppxMicrosoft_XboxIdentityProvider": {
"Category": "Xbox & Gaming",
"Content": "Xbox Identity Provider",
"Description": "Manages Xbox network user authentication and background account validation for connected titles. Warning: removing this may break Microsoft account sign-in for non-Xbox games and apps that rely on this authentication pipeline.",
"Panel": "1",
"PackageId": "Microsoft.XboxIdentityProvider"
"PackageId": "Microsoft.XboxIdentityProvider",
"StoreId": "9WZDNCRD1HKW"
},
"WPFAppxMicrosoft_XboxSpeechToTextOverlay": {
"Category": "Xbox & Gaming",
@@ -221,7 +250,8 @@
"Content": "Start Experiences App",
"Description": "Powers the Windows Widgets board, delivering a personalized feed of news, weather, sports, and finance content.",
"Panel": "1",
"PackageId": "Microsoft.StartExperiencesApp"
"PackageId": "Microsoft.StartExperiencesApp",
"StoreId": "9PC1H9VN18CM"
},
"WPFAppxMicrosoft_MicrosoftSolitaireCollection": {
"Category": "Xbox & Gaming",
+16 -8
View File
@@ -3,48 +3,56 @@
"Primary": "8.8.8.8",
"Secondary": "8.8.4.4",
"Primary6": "2001:4860:4860::8888",
"Secondary6": "2001:4860:4860::8844"
"Secondary6": "2001:4860:4860::8844",
"DohTemplate": "https://dns.google/dns-query"
},
"Cloudflare":{
"Primary": "1.1.1.1",
"Secondary": "1.0.0.1",
"Primary6": "2606:4700:4700::1111",
"Secondary6": "2606:4700:4700::1001"
"Secondary6": "2606:4700:4700::1001",
"DohTemplate": "https://cloudflare-dns.com/dns-query"
},
"Cloudflare_Malware":{
"Primary": "1.1.1.2",
"Secondary": "1.0.0.2",
"Primary6": "2606:4700:4700::1112",
"Secondary6": "2606:4700:4700::1002"
"Secondary6": "2606:4700:4700::1002",
"DohTemplate": "https://security.cloudflare-dns.com/dns-query"
},
"Cloudflare_Malware_Adult":{
"Primary": "1.1.1.3",
"Secondary": "1.0.0.3",
"Primary6": "2606:4700:4700::1113",
"Secondary6": "2606:4700:4700::1003"
"Secondary6": "2606:4700:4700::1003",
"DohTemplate": "https://family.cloudflare-dns.com/dns-query"
},
"Open_DNS":{
"Primary": "208.67.222.222",
"Secondary": "208.67.220.220",
"Primary6": "2620:119:35::35",
"Secondary6": "2620:119:53::53"
"Secondary6": "2620:119:53::53",
"DohTemplate": "https://doh.opendns.com/dns-query"
},
"Quad9":{
"Primary": "9.9.9.9",
"Secondary": "149.112.112.112",
"Primary6": "2620:fe::fe",
"Secondary6": "2620:fe::9"
"Secondary6": "2620:fe::9",
"DohTemplate": "https://dns.quad9.net/dns-query"
},
"AdGuard_Ads_Trackers":{
"Primary": "94.140.14.14",
"Secondary": "94.140.15.15",
"Primary6": "2a10:50c0::ad1:ff",
"Secondary6": "2a10:50c0::ad2:ff"
"Secondary6": "2a10:50c0::ad2:ff",
"DohTemplate": "https://dns.adguard-dns.com/dns-query"
},
"AdGuard_Ads_Trackers_Malware_Adult":{
"Primary": "94.140.14.15",
"Secondary": "94.140.15.16",
"Primary6": "2a10:50c0::bad1:ff",
"Secondary6": "2a10:50c0::bad2:ff"
"Secondary6": "2a10:50c0::bad2:ff",
"DohTemplate": "https://family.adguard-dns.com/dns-query"
}
}
+2 -6
View File
@@ -25,7 +25,7 @@
"IconFontSize": "14",
"IconButtonSize": "35",
"SettingsIconFontSize": "18",
"CloseIconFontSize": "18",
"CloseIconFontSize": "12",
"GroupBorderBackgroundColor": "#232629",
"ButtonFontSize": "12",
"ButtonFontFamily": "Arial",
@@ -46,7 +46,6 @@
"AppInstallUnselectedColor": "#F7F7F7",
"AppInstallHighlightedColor": "#CFCFCF",
"AppInstallSelectedColor": "#C2C2C2",
"AppInstallOverlayBackgroundColor": "#6A6D72",
"ComboBoxForegroundColor": "#232629",
"ComboBoxBackgroundColor": "#F7F7F7",
"LabelboxForegroundColor": "#232629",
@@ -60,7 +59,6 @@
"ScrollBarDraggingColor": "#6A6D72",
"ProgressBarForegroundColor": "#2E77FF",
"ProgressBarBackgroundColor": "Transparent",
"ProgressBarTextColor": "#232629",
"ButtonInstallBackgroundColor": "#F7F7F7",
"ButtonTweaksBackgroundColor": "#F7F7F7",
"ButtonConfigBackgroundColor": "#F7F7F7",
@@ -88,7 +86,6 @@
"AppInstallUnselectedColor": "#232629",
"AppInstallHighlightedColor": "#3C3C3C",
"AppInstallSelectedColor": "#4C4C4C",
"AppInstallOverlayBackgroundColor": "#2E3135",
"ComboBoxForegroundColor": "#F7F7F7",
"ComboBoxBackgroundColor": "#1E3747",
"LabelboxForegroundColor": "#5BDCFF",
@@ -100,9 +97,8 @@
"ScrollBarBackgroundColor": "#2E3135",
"ScrollBarHoverColor": "#3B4252",
"ScrollBarDraggingColor": "#5E81AC",
"ProgressBarForegroundColor": "#222222",
"ProgressBarForegroundColor": "#6EFF72",
"ProgressBarBackgroundColor": "Transparent",
"ProgressBarTextColor": "#232629",
"ButtonInstallBackgroundColor": "#222222",
"ButtonTweaksBackgroundColor": "#333333",
"ButtonConfigBackgroundColor": "#444444",
+1 -1
View File
@@ -650,7 +650,7 @@
icacls $Env:OneDrive /deny \"Administrators:(D,DC)\"
Write-Host \"Uninstalling OneDrive...\"
Start-Process '$Env:SystemRoot\\System32\\OneDriveSetup.exe' -ArgumentList '/uninstall' -Wait
Start-Process -FilePath (Join-Path $Env:SystemRoot \"System32\\OneDriveSetup.exe\") -ArgumentList '/uninstall' -Wait
# Some of OneDrive files use explorer, and OneDrive uses FileCoAuth
Write-Host \"Removing leftover OneDrive Files...\"
Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

+88 -119
View File
@@ -2,152 +2,121 @@
toc: true
---
## Contributing Code
# How to Contribute?
### Before You Start
## Testing
- Keep each pull request focused on a single feature or fix.
- Avoid unnecessary formatting changes or large unrelated edits.
- Document what changed and why in your PR description.
* Test the latest changes to WinUtil by running the pre-release and reporting issues you are encountering to help us continually improve WinUtil!
---
#### Run the latest pre-release
## Basic Git Workflow
### 1. Fork the Repository
Go to the ChrisTitusTech/winutil repository on GitHub and click the Fork button in the top right corner.
<img width="171" height="50" alt="{650A4723-F38A-44A4-9820-D232BC87C8A0}" src="https://github.com/user-attachments/assets/a214f27c-2fee-444a-920f-d87b14f5896f" />
---
### 2. Clone Your Fork
```bash
git clone https://github.com/YOUR_USERNAME/winutil.git
cd winutil
```ps1
irm https://christitus.com/windev | iex
```
---
> [!IMPORTANT]
> **Keep in mind** That this is a pre-release and should be treated as such. It exists for developers to test the utility and report or fix bugs before they get added to the stable release. Don't use it in production!
### 3. Create a Branch
## Issues
Never work directly on `main`.
* If you encounter any challenges or problems with the script, I kindly request that you submit them via the "Issues" tab on the GitHub repository. By filling out the provided template, you can provide specific details about the issue, allowing me (and others in the community) to promptly address any bugs or consider feature requests.
Create a branch related to your change:
## Contribute Code
```bash
git checkout -b feature-name
* Pull requests are now handled directly on the **MAIN branch**. This was done since we can now select specific releases to launch via releases in GitHub.
* If you're doing code changes, then you can submit a PR to the `main` branch.
> [!IMPORTANT]
> Do not use a code formatter, make massive amounts of line changes, or make multiple feature changes. EACH FEATURE CHANGE SHOULD BE IT'S OWN PULL REQUEST!
> [!IMPORTANT]
> Do not open a pull request that adds support for other languages to WinUtil for now, until we decide how we want to move forward with language support.
* When creating pull requests, it is essential to thoroughly document all changes made. This includes, but is not limited to, documenting any additions made to the `tweaks` section and corresponding `undo tweak`, so users are able to remove the newly added tweaks if necessary, and comprehensive documentation is required for all code changes. Document your changes and briefly explain why you made your changes in your Pull Request Description. Failure to adhere to this format may result in the denial of the pull request. Additionally, any code lacking sufficient documentation may also be denied.
* By following these guidelines, we can maintain a high standard of quality and ensure that the codebase remains organized and well-documented.
> [!NOTE]
>
> When creating a function, please include "WPF" or "WinUtil" in the file name so it can be loaded into the runspace.
## Walk through
* This is a guide for beginners. If you are still having issues, look at the following official GitHub documentation:
* [Commit through WEB](https://docs.github.com/en/pull-requests/committing-changes-to-your-project/creating-and-editing-commits/about-commits)
* [Commit through GitHub Desktop](https://docs.github.com/en/desktop/making-changes-in-a-branch/committing-and-reviewing-changes-to-your-project-in-github-desktop#about-commits)
* [Create a Pull Request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request)
### Overview
```mermaid
%%{init: {"flowchart": {"curve": "cardinal"}} }%%
graph TD
A[Fork Project] --> B[Clone Repository];
B --> C[Create New Branch];
C --> D[Make Changes];
D --> G[Test Changes];
G --> H{Tests Passed?};
H -->|Yes| E[Commit Changes];
H -->|No| J[Fix Issues];
J --> G;
E --> F[Push Branch];
F --> K[Create Pull Request];
K --> L[Fill out PR template];
classDef default stroke:#333,stroke-width:4px,font-size:12pt;
```
Example:
> [!NOTE]
>
> This is a diagram to guide you through the process. It may vary depending on the type of change you're making.
```bash
git checkout -b add-firefox-tweak
```
### Fork the Repo
* Fork the WinUtil Repository [here](https://github.com/ChrisTitusTech/winutil) to create a copy that will be available in your repository list.
---
{{< image src="images/Fork-Button" alt="Fork Image" >}}
### 4. Edit the Code
### Clone the Fork
> [!TIP]
>
> While you can make your changes directly through the Web, we recommend cloning the repo to your device using the application GitHub Desktop (available in WinUtil) to test your fork easily.
Open the project in your preferred text editor and make your changes.
* Install GitHub Desktop if it is not already installed.
* Log in using the same GitHub account you used to fork WinUtil.
* Choose the fork under "Your Repositories" and press "clone {repo name}"
* Create a new branch and name it something relatable to your changes.
Keep changes small and focused.
* Now you can modify WinUtil to your liking using your preferred text editor.
---
### 5. Test Your Changes
### Testing your changes
Open PowerShell as Administrator.
* To test to see if your changes work as intended, run the following commands in a PowerShell terminal as admin:
Go to the project folder:
* Change the directory where you are running the commands to the forked project.
* `cd {path to the folder with the compile.ps1}`
* Run the following command to compile and run WinUtil:
* `.\Compile.ps1 -run`
```powershell
cd path\to\winutil
```
{{< image src="images/Complie" alt="Compile" >}}
Run:
* After seeing that your changes work properly, feel free to commit the changes to the repository and make a PR. For help on that, follow the documentation below.
```powershell
.\Compile.ps1 -Run
```
### Committing the changes
* Before committing your changes, please discard changes made to the `winutil.ps1` file, like the following:
Verify:
{{< image src="images/Discard-GHD" alt="Push Commit Image" >}}
- WinUtil launches correctly
- Your feature works
- Nothing else breaks
* Now, commit your changes once you are happy with the result.
If something fails, fix it before committing.
{{< image src="images/Commit-GHD" alt="Commit Image" >}}
---
* Push the changes to upload them to your fork on github.com.
### 6. Review Your Changes
{{< image src="images/Push-Commit" alt="Push Commit Image" >}}
Check what changed:
```bash
git status
```
Review the diff:
```bash
git diff
```
Make sure you did not accidentally modify unrelated files.
---
### 7. Commit Your Changes
Stage files:
```bash
git add .
```
Commit them:
```bash
git commit -m "Add feature description"
```
Example:
```bash
git commit -m "Add Firefox package tweak"
```
---
### 8. Push Your Branch
```bash
git push origin branch-name
```
Example:
```bash
git push origin add-firefox-tweak
```
---
### 9. Open a Pull Request
Go to your fork on GitHub.
GitHub will show a button to create a pull request.
<img width="1009" height="71" alt="{C8C6A3CC-79D4-44FD-A54C-4C5717F12730}" src="https://github.com/user-attachments/assets/0419d193-d4e7-47c0-87cf-b986742201a0" />
Before submitting:
- Explain what changed
- Explain why you changed it
- Make sure unrelated files are not included
Once submitted, maintainers will review your PR.
### Making a PR
* To make a PR on your repo under a new branch linking to the main branch, a button will show and say Preview and Create pull request. Click that button and fill in all the information that is provided on the template. Once all the information is filled in correctly, check your PR to make sure there is no WinUtil.ps1 file attached to the PR. Once everything is good, make the PR and wait for Chris (the maintainer) to accept or deny your PR. Once it is accepted by Chris, you will be able to see your changes in the "/windev" build.
* If you do not see your feature in the main "/win" build, that is fine. All new changes go into the /windev build to make sure everything is working OK before going fully public.
* Congratulations! You just submitted your first PR. Thank you so much for contributing to WinUtil.
@@ -5,15 +5,53 @@ description: ""
```powershell {filename="functions/public/Invoke-WPFOOSU.ps1",linenos=inline,linenostart=1}
function Invoke-WPFOOSU {
try {
$ProgressPreference = 'SilentlyContinue'
if ($sync.ProcessRunning) {
Show-WinUtilMessage -Message "Another process is currently running." -Title "WinUtil" -Button "OK" -Icon "Warning"
return
}
Invoke-WebRequest -Uri https://dl5.oo-software.com/files/ooshutup10/OOSU10.exe -OutFile "$winutildir\ooshutup10.exe"
Start-Process -FilePath "$winutildir\ooshutup10.exe"
$downloadPath = Join-Path $sync.winutildir "ooshutup10.exe"
$sync.ProcessRunning = $true
$ProgressPreference = 'Continue'
} catch {
Write-Error "Couldn't download O&O ShutUp10. Please make sure you have an active Internet connection."
Invoke-WPFRunspace -ParameterList @(,("downloadPath", $downloadPath)) -ScriptBlock {
param($downloadPath)
$hasUI = $null -ne $sync.Form -and $null -ne $sync.Form.Dispatcher
try {
Write-WinUtilLog -Component "OOSU" -Message "Downloading O&O ShutUp10++."
if ($hasUI) {
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "Downloading O&O ShutUp10++ (0%)" -Percent 0
}
Save-WinUtilFile -Uri "https://dl5.oo-software.com/files/ooshutup10/OOSU10.exe" -DestinationPath $downloadPath -ProgressCallback {
param($percent)
if ($hasUI) {
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "Downloading O&O ShutUp10++ ($percent%)" -Percent $percent
}
}
if ($hasUI) {
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "Launching O&O ShutUp10++" -Percent 100
}
Start-Process -FilePath $downloadPath
Write-WinUtilLog -Component "OOSU" -Message "O&O ShutUp10++ launched."
if ($hasUI) {
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "O&O ShutUp10++ launched" -Percent 100
}
}
catch {
Write-WinUtilLog -Level "ERROR" -Component "OOSU" -Message "O&O ShutUp10++ download failed: $($_.Exception.Message)"
if ($hasUI) {
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "O&O ShutUp10++ download failed" -Percent 100
}
Write-Error "Couldn't download O&O ShutUp10. Please make sure you have an active Internet connection."
}
finally {
$sync.ProcessRunning = $false
}
}
}
```
@@ -15,7 +15,7 @@ description: ""
icacls $Env:OneDrive /deny \"Administrators:(D,DC)\"
Write-Host \"Uninstalling OneDrive...\"
Start-Process '$Env:SystemRoot\\System32\\OneDriveSetup.exe' -ArgumentList '/uninstall' -Wait
Start-Process -FilePath (Join-Path $Env:SystemRoot \"System32\\OneDriveSetup.exe\") -ArgumentList '/uninstall' -Wait
# Some of OneDrive files use explorer, and OneDrive uses FileCoAuth
Write-Host \"Removing leftover OneDrive Files...\"
@@ -138,7 +138,7 @@ For a better Windows experience with minimal risk:
1. Check multiple application boxes
2. All checked apps will install in sequence
3. Progress is shown in the bottom panel
3. Install and uninstall progress is shown in the window-level bottom panel, including the current package or package-manager batch and overall completion
### Applying Tweaks
+8
View File
@@ -34,6 +34,14 @@ Use the quick-selection buttons at the top of the Tweaks tab to speed up setup:
* **Select Tweaks to Remove**: Choose the tweaks you want to disable or remove.
* **Undo Tweaks**: Click **Undo Selected Tweaks** at the bottom of the screen to apply the changes.
### AppX Packages
Open **AppX Removal** from the Tweaks tab to manage the listed Windows apps. Select one or more packages, then choose **Install Selected** or **Remove Selected**.
When installing a selected package, WinUtil first registers an existing local `AppxManifest.xml`. If no usable local manifest remains, WinUtil installs the package from the Microsoft Store through WinGet when a Store product ID is available.
During AppX installation or removal, the window-level progress bar shows the current package, completed package count, and overall progress. The Windows taskbar also reflects progress and the final success or failure state.
### Essential Tweaks
Essential Tweaks are the safest starting point for most systems. They focus on lower-risk changes that improve usability, reduce noise, and avoid the more invasive changes found in advanced options.
+14 -12
View File
@@ -9,28 +9,30 @@ WinUtil provides three update modes so you can choose how aggressively Windows U
Changing modes adjusts system-wide Windows Update behavior. After switching modes, give Windows a moment to apply the policy and plan for a restart if the new state does not appear immediately.
{{< image src="images/updates-tab-new" alt="Updates tab in WinUtil" >}}
- **Recommended**: Prioritizes stability while still receiving security updates
- **Windows Default**: Restores standard Windows Update behavior
- **Disable Updates**: Blocks Windows Update and should only be used with extreme caution
- **Default (Out of the Box) Settings**: Restores standard Windows Update behavior
- **Security (Recommended) Settings**: Prioritizes stability while still receiving security updates
- **Disable ALL Updates**: Turns off Windows Update entirely and should only be used with extreme caution
### Windows Default
### Default (Out of Box) Settings
- **What it does**: Restores the default Windows Update configuration.
- **What it does**: Removes Windows Update policies managed by WinUtil, restores update service startup settings, and re-enables update scheduled tasks.
- **Best for**: Systems where you want Windows to manage updates normally.
- **Notes**: This removes custom update settings previously applied by WinUtil. If update errors continue, use the reset option in the **Config** tab to restore Microsoft Update services to their default state.
- **Notes**: Only values managed by WinUtil are removed; other Windows Update policies are left in place. If update errors continue, use the reset option in the **Config** tab to repair Microsoft Update components.
### Security (Recommended) Settings
### Recommended
- **What it does**: Applies a more conservative update strategy designed for most users.
- **Feature updates**: Delayed by **365 days** to reduce the chance of disruption from major Windows changes.
- **Security updates**: Delayed by **4 days** to allow time for early issues to surface while still keeping the system protected.
- **Quality updates**: Delayed by **4 days** to allow time for early issues to surface while still keeping the system protected.
- **Drivers**: Excluded from Windows quality updates.
- **Restarts**: Scheduled updates do not automatically restart Windows while a user is signed in. A restart explicitly scheduled by a user still takes precedence.
- **Availability**: Update deferral policies apply to Windows Pro, Enterprise, and Education editions.
- **Why use it**: This mode offers the best balance between security and stability, which is why it is the recommended option for most PCs.
### Disable ALL Updates (NOT RECOMMENDED!)
### Disable Updates (NOT RECOMMENDED!)
- **What it does**: Disables all Windows updates.
- **What it does**: Disables automatic update policy, stops and disables update services, disables update scheduled tasks, and clears downloaded update files.
- **Best for**: Highly controlled or special-purpose systems where updates must remain off temporarily.
- **Warning**: This leaves the system without security patches and significantly increases security risk.
- **Notes**: Windows servicing can restore update components in some circumstances. Use **Restore Defaults** when you are ready to receive updates again.
- **Recommendation**: Avoid this mode unless you fully understand the tradeoffs and have a specific reason to use it.
@@ -10,6 +10,9 @@ function Find-AppsByNameOrDescription {
.PARAMETER SearchString
The string to be searched for. Wildcards are treated as literal characters.
.PARAMETER Category
When provided, only applications in this exact category are shown.
.NOTES
- Uses module-scope $sync (no parameter needed; inherits from caller's scope)
- Performs literal matching (no wildcard expansion)
@@ -18,7 +21,10 @@ function Find-AppsByNameOrDescription {
#>
param(
[Parameter(Mandatory = $false)]
[string]$SearchString = ""
[string]$SearchString = "",
[Parameter(Mandatory = $false)]
[string]$Category = ""
)
# Validate that $sync exists and has required structure
@@ -39,7 +45,7 @@ function Find-AppsByNameOrDescription {
try {
# Reset the visibility if the search string is empty or the search is cleared
if ([string]::IsNullOrWhiteSpace($SearchString)) {
if ([string]::IsNullOrWhiteSpace($SearchString) -and [string]::IsNullOrWhiteSpace($Category)) {
$sync.ItemsControl.Items | ForEach-Object {
# Each item is a StackPanel container
$_.Visibility = [Windows.Visibility]::Visible
@@ -94,10 +100,11 @@ function Find-AppsByNameOrDescription {
# Check if app matches search criteria
if ($null -ne $appEntry) {
$contentMatch = $appEntry.Content -like "*$escapedSearchString*"
$descriptionMatch = $appEntry.Description -like "*$escapedSearchString*"
$categoryMatch = -not [string]::IsNullOrWhiteSpace($Category) -and $appEntry.Category -eq $Category
$contentMatch = [string]::IsNullOrWhiteSpace($Category) -and $appEntry.Content -like "*$escapedSearchString*"
$descriptionMatch = [string]::IsNullOrWhiteSpace($Category) -and $appEntry.Description -like "*$escapedSearchString*"
if ($contentMatch -or $descriptionMatch) {
if ($categoryMatch -or $contentMatch -or $descriptionMatch) {
# Show the App and mark that this category has a match
$appControl.Visibility = [Windows.Visibility]::Visible
$categoryHasMatch = $true
@@ -0,0 +1,24 @@
function Get-WinUtilInstalledAPPX {
<#
.SYNOPSIS
Gets the names of AppX packages installed for all users
#>
# AppX module auto-loading can leave PowerShell 7 dependent on a temporary Windows PowerShell
# compatibility proxy. Run the query in Windows PowerShell 5.1 so it remains available after
# those temporary proxy files are removed.
$ps5Command = {
Get-AppxPackage -AllUsers -ErrorAction Stop | Select-Object -ExpandProperty Name
}
$packageOutput = powershell.exe -NoProfile -NonInteractive -Command $ps5Command 2>&1
if ($LASTEXITCODE -ne 0) {
$failureDetails = ($packageOutput | Out-String).Trim()
Write-WinUtilLog -Level "ERROR" -Component "AppX" -Message "Failed to get installed AppX packages: $failureDetails"
return @()
}
return @($packageOutput)
}
@@ -1,12 +0,0 @@
function Hide-WPFInstallAppBusy {
<#
.SYNOPSIS
Hides the busy overlay in the install app area of the WPF form.
This is used to indicate that an install or uninstall has finished.
#>
Invoke-WPFUIThread -ScriptBlock {
$sync.InstallAppAreaOverlay.Visibility = [Windows.Visibility]::Collapsed
$sync.InstallAppAreaBorder.IsEnabled = $true
$sync.InstallAppAreaScrollViewer.Effect.Radius = 0
}
}
@@ -5,8 +5,6 @@
This is used as the parent object for all category and app entries on the install tab
Used to as part of the Install Tab UI generation
Also creates an overlay with a progress bar and text to indicate that an install or uninstall is in progress
.PARAMETER TargetElement
The element to which the AppArea should be added
@@ -19,22 +17,14 @@
$Border = New-Object Windows.Controls.Border
$Border.VerticalAlignment = "Stretch"
$Border.SetResourceReference([Windows.Controls.Control]::StyleProperty, "BorderStyle")
$sync.InstallAppAreaBorder = $Border
# Add a ScrollViewer, because the ItemsControl does not support scrolling by itself
$scrollViewer = New-Object Windows.Controls.ScrollViewer
$scrollViewer.VerticalScrollBarVisibility = 'Auto'
$scrollViewer.HorizontalAlignment = 'Stretch'
$scrollViewer.VerticalAlignment = 'Stretch'
$scrollViewer.CanContentScroll = $true
$sync.InstallAppAreaScrollViewer = $scrollViewer
$Border.Child = $scrollViewer
# Initialize the Blur Effect for the ScrollViewer, which will be used to indicate that an install/uninstall is in progress
$blurEffect = New-Object Windows.Media.Effects.BlurEffect
$blurEffect.Radius = 0
$scrollViewer.Effect = $blurEffect
## Create the ItemsControl, which will be the parent of all the app entries
$itemsControl = New-Object Windows.Controls.ItemsControl
$itemsControl.HorizontalAlignment = 'Stretch'
@@ -52,61 +42,5 @@
# Add the Border containing the App Area to the target Grid
$targetGrid.Children.Add($Border) | Out-Null
$overlay = New-Object Windows.Controls.Border
$overlay.CornerRadius = New-Object Windows.CornerRadius(10)
$overlay.SetResourceReference([Windows.Controls.Control]::BackgroundProperty, "AppInstallOverlayBackgroundColor")
$overlay.Visibility = [Windows.Visibility]::Collapsed
# Also add the overlay to the target Grid on top of the App Area
$targetGrid.Children.Add($overlay) | Out-Null
$sync.InstallAppAreaOverlay = $overlay
$overlayText = New-Object Windows.Controls.TextBlock
$overlayText.Text = "Installing apps..."
$overlayText.HorizontalAlignment = 'Center'
$overlayText.VerticalAlignment = 'Center'
$overlayText.SetResourceReference([Windows.Controls.TextBlock]::ForegroundProperty, "MainForegroundColor")
$overlayText.Background = "Transparent"
$overlayText.SetResourceReference([Windows.Controls.TextBlock]::FontSizeProperty, "HeaderFontSize")
$overlayText.SetResourceReference([Windows.Controls.TextBlock]::FontFamilyProperty, "MainFontFamily")
$overlayText.SetResourceReference([Windows.Controls.TextBlock]::FontWeightProperty, "MainFontWeight")
$overlayText.SetResourceReference([Windows.Controls.TextBlock]::MarginProperty, "MainMargin")
$sync.InstallAppAreaOverlayText = $overlayText
$progressbar = New-Object Windows.Controls.ProgressBar
$progressbar.Name = "ProgressBar"
$progressbar.Width = 250
$progressbar.Height = 50
$sync.ProgressBar = $progressbar
# Add a TextBlock overlay for the progress bar text
$progressBarTextBlock = New-Object Windows.Controls.TextBlock
$progressBarTextBlock.Name = "progressBarTextBlock"
$progressBarTextBlock.FontWeight = [Windows.FontWeights]::Bold
$progressBarTextBlock.FontSize = 16
$progressBarTextBlock.Width = $progressbar.Width
$progressBarTextBlock.Height = $progressbar.Height
$progressBarTextBlock.SetResourceReference([Windows.Controls.TextBlock]::ForegroundProperty, "ProgressBarTextColor")
$progressBarTextBlock.TextTrimming = "CharacterEllipsis"
$progressBarTextBlock.Background = "Transparent"
$sync.progressBarTextBlock = $progressBarTextBlock
# Create a Grid to overlay the text on the progress bar
$progressGrid = New-Object Windows.Controls.Grid
$progressGrid.Width = $progressbar.Width
$progressGrid.Height = $progressbar.Height
$progressGrid.Margin = "0,10,0,10"
$progressGrid.Children.Add($progressbar) | Out-Null
$progressGrid.Children.Add($progressBarTextBlock) | Out-Null
$overlayStackPanel = New-Object Windows.Controls.StackPanel
$overlayStackPanel.Orientation = "Vertical"
$overlayStackPanel.HorizontalAlignment = 'Center'
$overlayStackPanel.VerticalAlignment = 'Center'
$overlayStackPanel.Children.Add($overlayText) | Out-Null
$overlayStackPanel.Children.Add($progressGrid) | Out-Null
$overlay.Child = $overlayStackPanel
return $itemsControl
}
@@ -28,7 +28,7 @@ function Initialize-WinUtilTabContent {
"AppX" {
Invoke-WPFUIElements -configVariable $sync.configs.appx -targetGridName "appxpanel" -columncount 2
}
"Win11 Creator" {
"Win11ISO" {
if ($sync.Form -and $sync.Form.Dispatcher) {
$sync.Form.Dispatcher.BeginInvoke([System.Windows.Threading.DispatcherPriority]::Background, [action]{ Invoke-WinUtilISOCheckExistingWork }) | Out-Null
}
+79
View File
@@ -0,0 +1,79 @@
function Install-WinUtilAPPX {
<#
.SYNOPSIS
Registers a local AppX package or installs it from the Microsoft Store
.PARAMETER Name
The AppX package name to install
.PARAMETER StoreId
The optional Microsoft Store product ID used when no local manifest is available
#>
param (
[Parameter(Mandatory = $true)]
[string]$Name,
[string]$StoreId
)
Write-WinUtilLog -Component "AppX" -Message "Installing AppX package: $Name"
# AppX and DISM cmdlets are more reliable in Windows PowerShell 5.1. Query both installed and
# provisioned package metadata because either can expose a local manifest that can be registered.
$ps5Command = {
$packageName = $args[0]
$manifestPaths = [System.Collections.Generic.List[string]]::new()
Get-AppxPackage -AllUsers -Name $packageName -ErrorAction SilentlyContinue |
Sort-Object -Property Version -Descending |
ForEach-Object {
if (-not [string]::IsNullOrWhiteSpace($_.InstallLocation)) {
$manifestPaths.Add((Join-Path $_.InstallLocation "AppxManifest.xml"))
}
}
Get-AppxProvisionedPackage -Online -ErrorAction SilentlyContinue |
Where-Object DisplayName -EQ $packageName |
ForEach-Object {
if (-not [string]::IsNullOrWhiteSpace($_.InstallLocation)) {
$manifestPaths.Add((Join-Path $_.InstallLocation "AppxManifest.xml"))
}
}
$manifestPath = $manifestPaths |
Select-Object -Unique |
Where-Object { Test-Path -LiteralPath $_ } |
Select-Object -First 1
if ($null -ne $manifestPath) {
Add-AppxPackage -Register $manifestPath -DisableDevelopmentMode -ErrorAction Stop
Write-Output $manifestPath
}
}
$manifestOutput = powershell.exe -NoProfile -NonInteractive -Command $ps5Command -args $Name 2>&1
if ($LASTEXITCODE -eq 0 -and $null -ne $manifestOutput) {
$manifestPath = ($manifestOutput | Select-Object -Last 1).ToString().Trim()
if (-not [string]::IsNullOrWhiteSpace($manifestPath)) {
Write-WinUtilLog -Component "AppX" -Message "Registered local AppX manifest for $Name`: $manifestPath"
return
}
}
if ($LASTEXITCODE -ne 0) {
$failureDetails = ($manifestOutput | Out-String).Trim()
Write-WinUtilLog -Level "WARN" -Component "AppX" -Message "Local AppX registration failed for $Name`: $failureDetails"
}
if ([string]::IsNullOrWhiteSpace($StoreId)) {
$errorMessage = "Unable to install $Name because no local manifest or Microsoft Store ID is available."
Write-WinUtilLog -Level "ERROR" -Component "AppX" -Message $errorMessage
throw $errorMessage
}
Write-WinUtilLog -Component "AppX" -Message "No usable local manifest found for $Name. Installing Microsoft Store product $StoreId."
Install-WinUtilWinget
Install-WinUtilProgramWinget -Action Install -Programs @("msstore:$StoreId")
}
@@ -15,32 +15,36 @@ Function Invoke-WinUtilCurrentSystem {
)
if ($CheckBox -eq "choco") {
$apps = (choco list | Select-String -Pattern "^\S+").Matches.Value
$filter = Get-WinUtilVariables -Type Checkbox | Where-Object {$psitem -like "WPFInstall*"}
$sync.GetEnumerator() | Where-Object {$psitem.Key -in $filter} | ForEach-Object {
$dependencies = @($sync.configs.applications.$($psitem.Key).choco -split ";")
if ($dependencies -in $apps) {
Write-Output $psitem.name
$sync.configs.applicationsHashtable.GetEnumerator() | ForEach-Object {
$packageId = ($_.Value.choco -split ";")[-1].Trim()
if ($packageId -ne "na" -and $packageId -in $apps) {
Write-Output $_.Key
}
}
}
if ($checkbox -eq "winget") {
$originalEncoding = [Console]::OutputEncoding
[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new()
$Sync.InstalledPrograms = @("winget", "msstore") | ForEach-Object {
winget list -s $psitem | Select-Object -skip 3 | ConvertFrom-String -PropertyNames "Name", "Id", "Version", "Available" -Delimiter '\s{2,}'
try {
[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new()
$installedProgramOutput = @(winget list --accept-source-agreements --disable-interactivity 2>&1)
if ($LASTEXITCODE -ne 0) {
throw "winget list failed with exit code $LASTEXITCODE."
}
} finally {
[Console]::OutputEncoding = $originalEncoding
}
[Console]::OutputEncoding = $originalEncoding
$installedProgramText = $installedProgramOutput -join "`n"
$filter = Get-WinUtilVariables -Type Checkbox | Where-Object {$psitem -like "WPFInstall*"}
$sync.GetEnumerator() | Where-Object {$psitem.Key -in $filter} | ForEach-Object {
$dependencies = @($sync.configs.applications.$($psitem.Key).winget -split ";") | ForEach-Object {
$psitem -replace "^msstore:", ""
$sync.configs.applicationsHashtable.GetEnumerator() | ForEach-Object {
$packageId = (($_.Value.winget -split ";")[-1] -replace "^msstore:", "").Trim()
if ([string]::IsNullOrWhiteSpace($packageId) -or $packageId -eq "na") {
return
}
if ($dependencies[-1] -in $sync.InstalledPrograms.Id) {
Write-Output $psitem.name
$packagePattern = "(?im)[^\S\r\n]{2,}$([regex]::Escape($packageId))(?=[^\S\r\n]{2,}|$)"
if ($installedProgramText -match $packagePattern) {
Write-Output $_.Key
}
}
}
+151 -266
View File
@@ -1,4 +1,4 @@
function Write-Win11ISOLog {
function Write-WinUtilISOLog {
param([string]$Message)
$ts = (Get-Date).ToString("HH:mm:ss")
$logLine = "[$ts] $Message"
@@ -35,7 +35,7 @@ function Invoke-WinUtilISOBrowse {
$sync["WPFWin11ISOModifySection"].Visibility = "Collapsed"
$sync["WPFWin11ISOOutputSection"].Visibility = "Collapsed"
Write-Win11ISOLog "ISO selected: $isoPath ($fileSizeGB GB)"
Write-WinUtilISOLog "ISO selected: $isoPath ($fileSizeGB GB)"
}
function Invoke-WinUtilISOMountAndVerify {
@@ -46,84 +46,102 @@ function Invoke-WinUtilISOMountAndVerify {
return
}
Write-Win11ISOLog "Mounting ISO: $isoPath"
Set-WinUtilProgressBar -Label "Mounting ISO..." -Percent 10
Write-WinUtilISOLog "Mounting ISO: $isoPath"
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "Mounting ISO..." -Percent 10
$sync["WPFWin11ISOBrowseButton"].IsEnabled = $false
$sync["WPFWin11ISOMountButton"].IsEnabled = $false
$sync["WPFWin11ISOModifyButton"].IsEnabled = $false
$sync["Win11ISOProcessRunning"] = $true
try {
Mount-DiskImage -ImagePath $isoPath
Invoke-WPFRunspace -ParameterList @(,('isoPath', $isoPath)) -ScriptBlock {
param($isoPath)
do {
Start-Sleep -Milliseconds 500
} until ((Get-DiskImage -ImagePath $isoPath | Get-Volume).DriveLetter)
try {
Mount-DiskImage -ImagePath $isoPath
$driveLetter = (Get-DiskImage -ImagePath $isoPath | Get-Volume).DriveLetter + ":"
Write-Win11ISOLog "Mounted at drive $driveLetter"
do {
Start-Sleep -Milliseconds 500
} until ((Get-DiskImage -ImagePath $isoPath | Get-Volume).DriveLetter)
Set-WinUtilProgressBar -Label "Verifying ISO contents..." -Percent 30
$driveLetter = (Get-DiskImage -ImagePath $isoPath | Get-Volume).DriveLetter + ":"
Write-WinUtilISOLog "Mounted at drive $driveLetter"
$wimPath = Join-Path $driveLetter "sources\install.wim"
$esdPath = Join-Path $driveLetter "sources\install.esd"
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "Verifying ISO contents..." -Percent 30
if (-not (Test-Path $wimPath) -and -not (Test-Path $esdPath)) {
Dismount-DiskImage -ImagePath $isoPath
Write-Win11ISOLog "ERROR: install.wim/install.esd not found - not a valid Windows ISO."
[System.Windows.MessageBox]::Show(
"This does not appear to be a valid Windows ISO.`n`ninstall.wim / install.esd was not found.",
"Invalid ISO", "OK", "Error")
Set-WinUtilProgressBar -Label "" -Percent 0
return
}
$wimPath = Join-Path $driveLetter "sources\install.wim"
$esdPath = Join-Path $driveLetter "sources\install.esd"
$activeWim = if (Test-Path $wimPath) { $wimPath } else { $esdPath }
Set-WinUtilProgressBar -Label "Reading image metadata..." -Percent 55
$imageInfo = Get-WindowsImage -ImagePath $activeWim | Select-Object ImageIndex, ImageName
if (-not ($imageInfo | Where-Object { $_.ImageName -match "Windows 11" })) {
Dismount-DiskImage -ImagePath $isoPath
Write-Win11ISOLog "ERROR: No 'Windows 11' edition found in the image."
[System.Windows.MessageBox]::Show(
"No Windows 11 edition was found in this ISO.`n`nOnly official Windows 11 ISOs are supported.",
"Not a Windows 11 ISO", "OK", "Error")
Set-WinUtilProgressBar -Label "" -Percent 0
return
}
$sync["Win11ISOImageInfo"] = $imageInfo
$sync["WPFWin11ISOMountDriveLetter"].Text = "Mounted at: $driveLetter | Image file: $(Split-Path $activeWim -Leaf)"
$sync["WPFWin11ISOEditionComboBox"].Dispatcher.Invoke([action]{
$sync["WPFWin11ISOEditionComboBox"].Items.Clear()
foreach ($img in $imageInfo) {
[void]$sync["WPFWin11ISOEditionComboBox"].Items.Add("$($img.ImageIndex): $($img.ImageName)")
}
if ($sync["WPFWin11ISOEditionComboBox"].Items.Count -gt 0) {
$proIndex = -1
for ($i = 0; $i -lt $sync["WPFWin11ISOEditionComboBox"].Items.Count; $i++) {
if ($sync["WPFWin11ISOEditionComboBox"].Items[$i] -match "Windows 11 Pro(?![\w ])") {
$proIndex = $i; break
}
if (-not (Test-Path $wimPath) -and -not (Test-Path $esdPath)) {
Dismount-DiskImage -ImagePath $isoPath
Write-WinUtilISOLog "ERROR: install.wim/install.esd not found - not a valid Windows ISO."
Invoke-WPFUIThread {
[System.Windows.MessageBox]::Show(
"This does not appear to be a valid Windows ISO.`n`ninstall.wim / install.esd was not found.",
"Invalid ISO", "OK", "Error")
}
$sync["WPFWin11ISOEditionComboBox"].SelectedIndex = if ($proIndex -ge 0) { $proIndex } else { 0 }
return
}
})
$sync["WPFWin11ISOVerifyResultPanel"].Visibility = "Visible"
$sync["Win11ISODriveLetter"] = $driveLetter
$sync["Win11ISOWimPath"] = $activeWim
$sync["Win11ISOImagePath"] = $isoPath
$sync["WPFWin11ISOModifySection"].Visibility = "Visible"
$activeWim = if (Test-Path $wimPath) { $wimPath } else { $esdPath }
Set-WinUtilProgressBar -Label "ISO verified" -Percent 100
Write-Win11ISOLog "ISO verified OK. Editions found: $($imageInfo.Count)"
} catch {
Write-Win11ISOLog "ERROR during mount/verify: $_"
[System.Windows.MessageBox]::Show(
"An error occurred while mounting or verifying the ISO:`n`n$_",
"Error", "OK", "Error")
} finally {
Start-Sleep -Milliseconds 800
Set-WinUtilProgressBar -Label "" -Percent 0
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "Reading image metadata..." -Percent 55
$imageInfo = Get-WindowsImage -ImagePath $activeWim | Select-Object ImageIndex, ImageName
if (-not ($imageInfo | Where-Object { $_.ImageName -match "Windows 11" })) {
Dismount-DiskImage -ImagePath $isoPath
Write-WinUtilISOLog "ERROR: No 'Windows 11' edition found in the image."
Invoke-WPFUIThread {
[System.Windows.MessageBox]::Show(
"No Windows 11 edition was found in this ISO.`n`nOnly official Windows 11 ISOs are supported.",
"Not a Windows 11 ISO", "OK", "Error")
}
return
}
$sync["Win11ISOImageInfo"] = $imageInfo
$sync["Win11ISODriveLetter"] = $driveLetter
$sync["Win11ISOWimPath"] = $activeWim
$sync["Win11ISOImagePath"] = $isoPath
Invoke-WPFUIThread {
$sync["WPFWin11ISOMountDriveLetter"].Text = "Mounted at: $driveLetter | Image file: $(Split-Path $activeWim -Leaf)"
$sync["WPFWin11ISOEditionComboBox"].Items.Clear()
foreach ($img in $imageInfo) {
[void]$sync["WPFWin11ISOEditionComboBox"].Items.Add("$($img.ImageIndex): $($img.ImageName)")
}
if ($sync["WPFWin11ISOEditionComboBox"].Items.Count -gt 0) {
$proIndex = -1
for ($i = 0; $i -lt $sync["WPFWin11ISOEditionComboBox"].Items.Count; $i++) {
if ($sync["WPFWin11ISOEditionComboBox"].Items[$i] -match "Windows 11 Pro(?![\w ])") {
$proIndex = $i; break
}
}
$sync["WPFWin11ISOEditionComboBox"].SelectedIndex = if ($proIndex -ge 0) { $proIndex } else { 0 }
}
$sync["WPFWin11ISOVerifyResultPanel"].Visibility = "Visible"
$sync["WPFWin11ISOModifySection"].Visibility = "Visible"
$sync["WPFWin11ISOModifyButton"].IsEnabled = $true
}
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "ISO verified" -Percent 100
Write-WinUtilISOLog "ISO verified OK. Editions found: $($imageInfo.Count)"
} catch {
$errorMessage = $_
Write-WinUtilISOLog "ERROR during mount/verify: $errorMessage"
Invoke-WPFUIThread {
[System.Windows.MessageBox]::Show(
"An error occurred while mounting or verifying the ISO:`n`n$errorMessage",
"Error", "OK", "Error")
}
} finally {
Start-Sleep -Milliseconds 800
Set-WinUtilTweaksProgressIndicator -Visible $false
Invoke-WPFUIThread {
$sync["WPFWin11ISOBrowseButton"].IsEnabled = $true
$sync["WPFWin11ISOMountButton"].IsEnabled = $true
$sync["Win11ISOProcessRunning"] = $false
}
}
}
}
@@ -147,10 +165,11 @@ function Invoke-WinUtilISOModify {
$selectedWimIndex = $sync["Win11ISOImageInfo"][0].ImageIndex
}
$selectedEditionName = if ($selectedItem) { ($selectedItem -replace '^\d+:\s*', '') } else { "Unknown" }
Write-Win11ISOLog "Selected edition: $selectedEditionName (Index $selectedWimIndex)"
Write-WinUtilISOLog "Selected edition: $selectedEditionName (Index $selectedWimIndex)"
$sync["WPFWin11ISOModifyButton"].IsEnabled = $false
$sync["Win11ISOModifying"] = $true
$sync["Win11ISOProcessRunning"] = $true
$workDir = Join-Path $env:TEMP "WinUtil_Win11ISO_$(Get-Date -Format 'yyyyMMdd_HHmmss')"
if (Test-Path $workDir) {
@@ -169,7 +188,6 @@ function Invoke-WinUtilISOModify {
$runspace.ThreadOptions = "ReuseThread"
$runspace.Open()
$injectDrivers = $sync["WPFWin11ISOInjectDrivers"].IsChecked -eq $true
$runspace.SessionStateProxy.SetVariable("sync", $sync)
$runspace.SessionStateProxy.SetVariable("isoPath", $isoPath)
$runspace.SessionStateProxy.SetVariable("driveLetter", $driveLetter)
@@ -181,7 +199,7 @@ function Invoke-WinUtilISOModify {
$runspace.SessionStateProxy.SetVariable("injectDrivers", $injectDrivers)
$isoScriptFuncDef = "function Invoke-WinUtilISOScript {`n" + ${function:Invoke-WinUtilISOScript}.ToString() + "`n}"
$win11ISOLogFuncDef = "function Write-Win11ISOLog {`n" + ${function:Write-Win11ISOLog}.ToString() + "`n}"
$win11ISOLogFuncDef = "function Write-WinUtilISOLog {`n" + ${function:Write-WinUtilISOLog}.ToString() + "`n}"
$runspace.SessionStateProxy.SetVariable("isoScriptFuncDef", $isoScriptFuncDef)
$runspace.SessionStateProxy.SetVariable("win11ISOLogFuncDef", $win11ISOLogFuncDef)
@@ -203,9 +221,10 @@ function Invoke-WinUtilISOModify {
function SetProgress($label, $pct) {
$sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{
$sync.progressBarTextBlock.Text = $label
$sync.progressBarTextBlock.ToolTip = $label
$sync.ProgressBar.Value = [Math]::Max($pct, 5)
$sync["WPFTweaksProgressBar"].Visibility = "Visible"
$sync["WPFTweaksProgressLabel"].Text = $label
$sync["WPFTweaksProgressLabel"].ToolTip = $label
$sync["WPFTweaksProgressValue"].Value = [Math]::Max($pct, 5)
})
}
@@ -233,121 +252,6 @@ function Invoke-WinUtilISOModify {
}
}
function Get-WinUtilMountedImageEditionId {
param(
[Parameter(Mandatory)][string]$MountDir,
[string]$EditionName,
[scriptblock]$Logger
)
try {
$dismOutput = & dism /English "/Image:$MountDir" /Get-CurrentEdition 2>&1
foreach ($line in $dismOutput) {
if ($line -match '^\s*Current Edition\s*:\s*(.+?)\s*$') {
$editionId = $Matches[1].Trim()
if ($editionId) {
if ($Logger) { $null = $Logger.Invoke("Detected mounted image EditionID: $editionId") }
return $editionId
}
}
}
} catch {
if ($Logger) { $null = $Logger.Invoke("Warning: could not detect mounted image EditionID with DISM: $_") }
}
$fallbackEditionId = Get-WinUtilEditionIdFromName -EditionName $EditionName
if ($fallbackEditionId -and $Logger) {
$null = $Logger.Invoke("Using fallback EditionID '$fallbackEditionId' from selected edition name.")
}
return $fallbackEditionId
}
function Get-DismImageInfoMap {
param(
[Parameter(Mandatory)][string]$ImagePath,
[int]$Index = 1
)
$map = @{}
$lines = & dism /English "/Get-ImageInfo" "/ImageFile:$ImagePath" "/Index:$Index"
foreach ($line in $lines) {
if ($line -match '^\s*([^:]+?)\s*:\s*(.*)$') {
$key = $Matches[1].Trim()
$val = $Matches[2].Trim()
if (-not $map.ContainsKey($key)) {
$map[$key] = $val
}
}
}
return $map
}
function Invoke-WinUtilWimMetadataHydration {
param(
[Parameter(Mandatory)][string]$ImagePath,
[Parameter(Mandatory)][string]$EditionName,
[scriptblock]$Logger
)
$metadataLogger = $Logger
function LogMeta([string]$Message) {
if ($metadataLogger) {
$null = $metadataLogger.Invoke($Message)
}
}
$before = Get-DismImageInfoMap -ImagePath $ImagePath -Index 1
$undefinedBefore = @($before.GetEnumerator() | Where-Object { $_.Value -eq '<undefined>' } | ForEach-Object { $_.Key })
if ($undefinedBefore.Count -eq 0) {
LogMeta "Metadata check: no undefined DISM fields detected."
return
}
LogMeta "Metadata check: undefined DISM fields detected: $($undefinedBefore -join ', ')"
LogMeta "Attempting best-effort metadata hydration for install.wim..."
$setImage = Get-Command Set-WindowsImage -ErrorAction SilentlyContinue
if (-not $setImage) {
LogMeta "Set-WindowsImage is unavailable on this host; cannot write additional WIM metadata fields."
return
}
$targetName = if ($EditionName -and $EditionName -ne 'Unknown') { $EditionName } else { $before['Name'] }
if (-not $targetName) { $targetName = 'Windows 11' }
$targetDescription = if ($before['Description'] -and $before['Description'] -ne '<undefined>') {
$before['Description']
} else {
$targetName
}
$setArgs = @{
ImagePath = $ImagePath
Index = 1
Name = $targetName
Description = $targetDescription
ErrorAction = 'Stop'
}
try {
Set-WindowsImage @setArgs | Out-Null
LogMeta "Applied Set-WindowsImage metadata updates (Name/Description)."
} catch {
LogMeta "Warning: Set-WindowsImage metadata update failed: $_"
}
$after = Get-DismImageInfoMap -ImagePath $ImagePath -Index 1
$undefinedAfter = @($after.GetEnumerator() | Where-Object { $_.Value -eq '<undefined>' } | ForEach-Object { $_.Key })
if ($undefinedAfter.Count -eq 0) {
LogMeta "Metadata hydration complete: no undefined DISM fields remain."
} else {
LogMeta "Metadata hydration complete. Remaining undefined DISM fields: $($undefinedAfter -join ', ')"
LogMeta "Note: some DISM metadata fields are read-only and come from Microsoft image internals."
}
}
try {
$sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{
$sync["WPFWin11ISOSelectSection"].Visibility = "Collapsed"
@@ -357,51 +261,30 @@ function Invoke-WinUtilISOModify {
Log "Creating working directory: $workDir"
$isoContents = Join-Path $workDir "iso_contents"
$mountDir = Join-Path $workDir "wim_mount"
New-Item -ItemType Directory -Path $isoContents, $mountDir -Force
New-Item -ItemType Directory -Path $isoContents -Force
SetProgress "Copying ISO contents..." 10
Log "Copying ISO contents from $driveLetter to $isoContents..."
& robocopy $driveLetter $isoContents /E /NFL /NDL /NJH /NJS
Log "ISO contents copied."
SetProgress "Mounting install.wim..." 25
SetProgress "Preparing setup media..." 25
$sourceImageFileName = Split-Path $wimPath -Leaf
$localWim = Join-Path $isoContents "sources\$sourceImageFileName"
if (-not (Test-Path $localWim)) {
throw "Copied ISO image file not found: sources\$sourceImageFileName"
}
Set-ItemProperty -Path $localWim -Name IsReadOnly -Value $false
$selectedEditionId = Get-WinUtilEditionIdFromName -EditionName $selectedEditionName
Log "Mounting install.wim (Index ${selectedWimIndex}: $selectedEditionName) at $mountDir..."
Mount-WindowsImage -ImagePath $localWim -Index $selectedWimIndex -Path $mountDir
SetProgress "Modifying install.wim..." 45
$selectedEditionId = Get-WinUtilMountedImageEditionId -MountDir $mountDir -EditionName $selectedEditionName -Logger ${function:Log}
Log "Writing autounattend.xml and edition selection..."
Invoke-WinUtilISOScript -ISOContentsDir $isoContents -AutoUnattendXml $autounattendContent -InjectCurrentSystemDrivers $injectDrivers -InstallImagePath $localWim -InstallImageIndex $selectedWimIndex -InstallEditionId $selectedEditionId -Log { param($m) Log $m }
Log "Applying WinUtil modifications to install.wim..."
Invoke-WinUtilISOScript -ScratchDir $mountDir -ISOContentsDir $isoContents -AutoUnattendXml $autounattendContent -InjectCurrentSystemDrivers $injectDrivers -InstallEditionId $selectedEditionId -InstallImageIndex 1 -Log { param($m) Log $m }
SetProgress "Cleaning up component store (WinSxS)..." 56
Log "Running DISM component store cleanup (/ResetBase)..."
& dism /English "/image:$mountDir" /Cleanup-Image /StartComponentCleanup /ResetBase | ForEach-Object { Log $_ }
Log "Component store cleanup complete."
SetProgress "Saving modified install.wim..." 65
Log "Dismounting and saving install.wim. This will take several minutes..."
Dismount-WindowsImage -Path $mountDir -Save
Log "install.wim saved."
SetProgress "Removing unused editions from install.wim..." 70
Log "Exporting edition '$selectedEditionName' (Index $selectedWimIndex) to a single-edition install.wim..."
$exportWim = Join-Path $isoContents "sources\install_export.wim"
Export-WindowsImage -SourceImagePath $localWim -SourceIndex $selectedWimIndex -DestinationImagePath $exportWim
Remove-Item -Path $localWim -Force
Rename-Item -Path $exportWim -NewName "install.wim" -Force
$localWim = Join-Path $isoContents "sources\install.wim"
Log "Unused editions removed. install.wim now contains only '$selectedEditionName'."
SetProgress "Hydrating WIM metadata..." 76
Invoke-WinUtilWimMetadataHydration -ImagePath $localWim -EditionName $selectedEditionName -Logger ${function:Log}
SetProgress "Preserving install image..." 70
if ($injectDrivers) {
Log "Added current-system drivers to $sourceImageFileName index $selectedWimIndex with one mount and commit."
} else {
Log "Preserved the original $sourceImageFileName without mounting, exporting, or modifying it."
}
SetProgress "Dismounting source ISO..." 80
Log "Dismounting original ISO..."
@@ -419,16 +302,6 @@ function Invoke-WinUtilISOModify {
} catch {
Log "ERROR during modification: $_"
try {
if (Test-Path $mountDir) {
$mountedImages = Get-WindowsImage -Mounted | Where-Object { $_.Path -eq $mountDir }
if ($mountedImages) {
Log "Cleaning up: dismounting install.wim (discarding changes)..."
Dismount-WindowsImage -Path $mountDir -Discard
}
}
} catch { Log "Warning: could not dismount install.wim during cleanup: $_" }
try {
$mountedISO = Get-DiskImage -ImagePath $isoPath
if ($mountedISO -and $mountedISO.Attached) {
@@ -452,10 +325,12 @@ function Invoke-WinUtilISOModify {
} finally {
Start-Sleep -Milliseconds 800
$sync["Win11ISOModifying"] = $false
$sync["Win11ISOProcessRunning"] = $false
$sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{
$sync.progressBarTextBlock.Text = ""
$sync.progressBarTextBlock.ToolTip = ""
$sync.ProgressBar.Value = 0
$sync["WPFTweaksProgressBar"].Visibility = "Collapsed"
$sync["WPFTweaksProgressLabel"].Text = ""
$sync["WPFTweaksProgressLabel"].ToolTip = ""
$sync["WPFTweaksProgressValue"].Value = 0
$sync["WPFWin11ISOModifyButton"].IsEnabled = $true
if ($sync["WPFWin11ISOOutputSection"].Visibility -ne "Visible") {
$sync["WPFWin11ISOSelectSection"].Visibility = "Visible"
@@ -494,9 +369,9 @@ function Invoke-WinUtilISOCheckExistingWork {
$sync["WPFWin11ISOOutputSection"].Visibility = "Visible"
$modified = $existingWorkDir.LastWriteTime.ToString("yyyy-MM-dd HH:mm")
Write-Win11ISOLog "Existing working directory found: $($existingWorkDir.FullName)"
Write-Win11ISOLog "Last modified: $modified - Skipping Steps 1-3 and resuming at Step 4."
Write-Win11ISOLog "Click 'Clean & Reset' if you want to start over with a new ISO."
Write-WinUtilISOLog "Existing working directory found: $($existingWorkDir.FullName)"
Write-WinUtilISOLog "Last modified: $modified - Skipping Steps 1-3 and resuming at Step 4."
Write-WinUtilISOLog "Click 'Clean & Reset' if you want to start over with a new ISO."
[System.Windows.MessageBox]::Show(
"A previous WinUtil ISO working directory was found:`n`n$($existingWorkDir.FullName)`n`n(Last modified: $modified)`n`nStep 4 (output options) has been restored so you can save the already-modified image.`n`nClick 'Clean & Reset' in Step 4 if you want to start over.",
@@ -514,6 +389,7 @@ function Invoke-WinUtilISOCleanAndReset {
}
$sync["WPFWin11ISOCleanResetButton"].IsEnabled = $false
$sync["Win11ISOProcessRunning"] = $true
$runspace = [Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace()
$runspace.ApartmentState = "STA"
@@ -538,9 +414,10 @@ function Invoke-WinUtilISOCleanAndReset {
function SetProgress($label, $pct) {
$sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{
$sync.progressBarTextBlock.Text = $label
$sync.progressBarTextBlock.ToolTip = $label
$sync.ProgressBar.Value = [Math]::Max($pct, 5)
$sync["WPFTweaksProgressBar"].Visibility = "Visible"
$sync["WPFTweaksProgressLabel"].Text = $label
$sync["WPFTweaksProgressLabel"].ToolTip = $label
$sync["WPFTweaksProgressValue"].Value = [Math]::Max($pct, 5)
})
}
@@ -628,20 +505,24 @@ function Invoke-WinUtilISOCleanAndReset {
$sync["WPFWin11ISOModifyButton"].IsEnabled = $true
$sync["WPFWin11ISOCleanResetButton"].IsEnabled = $true
$sync.progressBarTextBlock.Text = ""
$sync.progressBarTextBlock.ToolTip = ""
$sync.ProgressBar.Value = 0
$sync["WPFTweaksProgressBar"].Visibility = "Collapsed"
$sync["WPFTweaksProgressLabel"].Text = ""
$sync["WPFTweaksProgressLabel"].ToolTip = ""
$sync["WPFTweaksProgressValue"].Value = 0
$sync["WPFWin11ISOStatusLog"].Text = "Ready. Please select a Windows 11 ISO to begin."
})
} catch {
Log "ERROR during Clean & Reset: $_"
$sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{
$sync.progressBarTextBlock.Text = ""
$sync.progressBarTextBlock.ToolTip = ""
$sync.ProgressBar.Value = 0
$sync["WPFTweaksProgressBar"].Visibility = "Collapsed"
$sync["WPFTweaksProgressLabel"].Text = ""
$sync["WPFTweaksProgressLabel"].ToolTip = ""
$sync["WPFTweaksProgressValue"].Value = 0
$sync["WPFWin11ISOCleanResetButton"].IsEnabled = $true
})
} finally {
$sync["Win11ISOProcessRunning"] = $false
}
})
@@ -680,32 +561,33 @@ function Invoke-WinUtilISOExport {
}
if (-not $oscdimg) {
Write-Win11ISOLog "oscdimg.exe not found. Attempting to install via winget..."
Write-WinUtilISOLog "oscdimg.exe not found. Attempting to install via winget..."
try {
# First ensure winget is installed and operational
Install-WinUtilWinget
$winget = Get-Command winget
$result = & $winget install -e --id Microsoft.OSCDIMG --accept-package-agreements --accept-source-agreements
Write-Win11ISOLog "winget output: $result"
Write-WinUtilISOLog "winget output: $result"
$oscdimg = Get-ChildItem "$env:LOCALAPPDATA\Microsoft\WinGet\Packages" -Recurse -Filter "oscdimg.exe" |
Where-Object { $_.FullName -match 'Microsoft\.OSCDIMG' } |
Select-Object -First 1 -ExpandProperty FullName
} catch {
Write-Win11ISOLog "winget not available or install failed: $_"
Write-WinUtilISOLog "winget not available or install failed: $_"
}
if (-not $oscdimg) {
Write-Win11ISOLog "oscdimg.exe still not found after install attempt."
Write-WinUtilISOLog "oscdimg.exe still not found after install attempt."
[System.Windows.MessageBox]::Show(
"oscdimg.exe could not be found or installed automatically.`n`nPlease install it manually:`n winget install -e --id Microsoft.OSCDIMG`n`nOr install the Windows ADK from:`nhttps://learn.microsoft.com/windows-hardware/get-started/adk-install",
"oscdimg Not Found", "OK", "Warning")
return
}
Write-Win11ISOLog "oscdimg.exe installed successfully."
Write-WinUtilISOLog "oscdimg.exe installed successfully."
}
$sync["WPFWin11ISOChooseISOButton"].IsEnabled = $false
$sync["Win11ISOProcessRunning"] = $true
$runspace = [Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace()
$runspace.ApartmentState = "STA"
@@ -716,7 +598,7 @@ function Invoke-WinUtilISOExport {
$runspace.SessionStateProxy.SetVariable("outputISO", $outputISO)
$runspace.SessionStateProxy.SetVariable("oscdimg", $oscdimg)
$win11ISOLogFuncDef = "function Write-Win11ISOLog {`n" + ${function:Write-Win11ISOLog}.ToString() + "`n}"
$win11ISOLogFuncDef = "function Write-WinUtilISOLog {`n" + ${function:Write-WinUtilISOLog}.ToString() + "`n}"
$runspace.SessionStateProxy.SetVariable("win11ISOLogFuncDef", $win11ISOLogFuncDef)
$script = [Management.Automation.PowerShell]::Create()
@@ -726,20 +608,21 @@ function Invoke-WinUtilISOExport {
function SetProgress($label, $pct) {
$sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{
$sync.progressBarTextBlock.Text = $label
$sync.progressBarTextBlock.ToolTip = $label
$sync.ProgressBar.Value = [Math]::Max($pct, 5)
$sync["WPFTweaksProgressBar"].Visibility = "Visible"
$sync["WPFTweaksProgressLabel"].Text = $label
$sync["WPFTweaksProgressLabel"].ToolTip = $label
$sync["WPFTweaksProgressValue"].Value = [Math]::Max($pct, 5)
})
}
try {
Write-Win11ISOLog "Exporting to ISO: $outputISO"
Write-WinUtilISOLog "Exporting to ISO: $outputISO"
SetProgress "Building ISO..." 10
$bootData = "2#p0,e,b`"$contentsDir\boot\etfsboot.com`"#pEF,e,b`"$contentsDir\efi\microsoft\boot\efisys.bin`""
$oscdimgArgs = @("-m", "-o", "-u2", "-udfver102", "-bootdata:$bootData", "-l`"CTOS_MODIFIED`"", "`"$contentsDir`"", "`"$outputISO`"")
Write-Win11ISOLog "Running oscdimg..."
Write-WinUtilISOLog "Running oscdimg..."
$psi = [System.Diagnostics.ProcessStartInfo]::new()
$psi.FileName = $oscdimg
@@ -756,7 +639,7 @@ function Invoke-WinUtilISOExport {
# Stream stdout line-by-line as oscdimg runs
while (-not $proc.StandardOutput.EndOfStream) {
$line = $proc.StandardOutput.ReadLine()
if ($line.Trim()) { Write-Win11ISOLog $line }
if ($line.Trim()) { Write-WinUtilISOLog $line }
}
$proc.WaitForExit()
@@ -764,17 +647,17 @@ function Invoke-WinUtilISOExport {
# Flush any stderr after process exits
$stderr = $proc.StandardError.ReadToEnd()
foreach ($line in ($stderr -split "`r?`n")) {
if ($line.Trim()) { Write-Win11ISOLog "[stderr]$line" }
if ($line.Trim()) { Write-WinUtilISOLog "[stderr]$line" }
}
if ($proc.ExitCode -eq 0) {
SetProgress "ISO exported" 100
Write-Win11ISOLog "ISO exported successfully: $outputISO"
Write-WinUtilISOLog "ISO exported successfully: $outputISO"
$sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{
[System.Windows.MessageBox]::Show("ISO exported successfully!`n`n$outputISO", "Export Complete", "OK", "Info")
})
} else {
Write-Win11ISOLog "oscdimg exited with code $($proc.ExitCode)."
Write-WinUtilISOLog "oscdimg exited with code $($proc.ExitCode)."
$sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{
[System.Windows.MessageBox]::Show(
"oscdimg exited with code $($proc.ExitCode).`nCheck the status log for details.",
@@ -782,16 +665,18 @@ function Invoke-WinUtilISOExport {
})
}
} catch {
Write-Win11ISOLog "ERROR during ISO export: $_"
Write-WinUtilISOLog "ERROR during ISO export: $_"
$sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{
[System.Windows.MessageBox]::Show("ISO export failed:`n`n$_", "Error", "OK", "Error")
})
} finally {
Start-Sleep -Milliseconds 800
$sync["Win11ISOProcessRunning"] = $false
$sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{
$sync.progressBarTextBlock.Text = ""
$sync.progressBarTextBlock.ToolTip = ""
$sync.ProgressBar.Value = 0
$sync["WPFTweaksProgressBar"].Visibility = "Collapsed"
$sync["WPFTweaksProgressLabel"].Text = ""
$sync["WPFTweaksProgressLabel"].ToolTip = ""
$sync["WPFTweaksProgressValue"].Value = 0
$sync["WPFWin11ISOChooseISOButton"].IsEnabled = $true
})
}
+462 -430
View File
@@ -1,220 +1,228 @@
function Invoke-WinUtilISOScript {
<#
.SYNOPSIS
Applies WinUtil modifications to a mounted Windows 11 install.wim image.
Prepares copied Windows setup media without modifying its install image.
.DESCRIPTION
Removes AppX bloatware and OneDrive, optionally injects all drivers exported from
the running system into install.wim and boot.wim (controlled by the
-InjectCurrentSystemDrivers switch), applies offline registry tweaks (hardware
bypass, privacy, OOBE, telemetry, update suppression), deletes CEIP/WU
scheduled-task definition files, and optionally writes autounattend.xml to the ISO
root and removes the support\ folder from the ISO contents directory.
All setup scripts embedded in the autounattend.xml <Extensions><File> nodes are
written directly into the WIM at their target paths under C:\Windows\Setup\Scripts\
to ensure they survive Windows Setup stripping unrecognised-namespace XML elements
from the Panther copy of the answer file.
Mounting/dismounting the WIM is the caller's responsibility (e.g. Invoke-WinUtilISO).
.PARAMETER ScratchDir
Mandatory. Full path to the directory where the Windows image is currently mounted.
Stages WinUtil's AppX removal, registry tweaks, and scheduled-task cleanup
in the answer file for first logon, writes sources\ei.cfg for the selected
edition, and optionally adds current-system drivers to one install.wim index.
.PARAMETER ISOContentsDir
Optional. Root directory of the extracted ISO contents. When supplied,
autounattend.xml is written here and the support\ folder is removed.
Root directory of the copied ISO contents.
.PARAMETER AutoUnattendXml
Optional. Full XML content for autounattend.xml. If empty, the OOBE bypass
file is skipped and a warning is logged.
.PARAMETER InjectCurrentSystemDrivers
Optional. When $true, exports all drivers from the running system and injects
them into install.wim and boot.wim index 2 (Windows Setup PE).
Defaults to $false.
Full XML content for autounattend.xml.
.PARAMETER InstallEditionId
Optional. Windows edition ID for the selected image, for example Professional
or Core. Used to write sources\ei.cfg so setup does not fall back to an
embedded firmware product key for a different edition.
Windows setup EditionID for sources\ei.cfg, for example Professional or Core.
.PARAMETER InstallImagePath
Copied install.wim to service when current-system driver injection is enabled.
.PARAMETER InstallImageIndex
Optional. Image index that setup should install from the final install.wim.
Win11 Creator exports the selected edition to a single-image WIM, so this
defaults to 1.
Selected edition index in install.wim.
.PARAMETER Log
Optional ScriptBlock for progress/status logging. Receives a single [string] argument.
.EXAMPLE
Invoke-WinUtilISOScript -ScratchDir "C:\Temp\wim_mount"
.EXAMPLE
Invoke-WinUtilISOScript `
-ScratchDir $mountDir `
-ISOContentsDir $isoRoot `
-AutoUnattendXml (Get-Content .\tools\autounattend.xml -Raw) `
-Log { param($m) Write-Host $m }
.NOTES
Author : Chris Titus @christitustech
GitHub : https://github.com/ChrisTitusTech
#>
param (
[Parameter(Mandatory)][string]$ScratchDir,
[string]$ISOContentsDir = "",
[Parameter(Mandatory)][string]$ISOContentsDir,
[string]$AutoUnattendXml = "",
[bool]$InjectCurrentSystemDrivers = $false,
[string]$InstallEditionId = "",
[string]$InstallImagePath = "",
[int]$InstallImageIndex = 1,
[scriptblock]$Log = { param($m) Write-Output $m }
)
function Set-ISOScriptReg {
param ([string]$Path, [string]$Name, [string]$Type, [string]$Value)
try {
& reg add $Path /v $Name /t $Type /d $Value /f
& $Log "Set registry value: $Path\$Name"
} catch {
& $Log "Error setting registry value: $_"
function Add-WinUtilISOStagedDrivers {
param (
[Parameter(Mandatory)][string]$ContentRoot,
[Parameter(Mandatory)][string]$InstallImagePath,
[Parameter(Mandatory)][int]$InstallImageIndex,
[scriptblock]$Logger
)
function Copy-WinUtilISODriverFolder {
param (
[Parameter(Mandatory)][string]$Source,
[Parameter(Mandatory)][string]$Destination
)
$folderName = Split-Path $Source -Leaf
$targetPath = Join-Path $Destination $folderName
$suffix = 1
while (Test-Path -LiteralPath $targetPath) {
$targetPath = Join-Path $Destination "${folderName}_$suffix"
$suffix++
}
Copy-Item -LiteralPath $Source -Destination $targetPath -Recurse -Force -ErrorAction Stop
return $targetPath
}
}
function Remove-ISOScriptReg {
param ([string]$path)
try {
& reg delete $path /f
& $Log "Removed registry key: $path"
} catch {
& $Log "Error removing registry key: $_"
function Test-WinUtilISOStorageDriver {
param ([Parameter(Mandatory)][System.IO.FileInfo]$InfFile)
if ($InfFile.BaseName -match '(?i)(iaahci|iastor|vmd|irst|rst)') {
return $true
}
try {
return (Get-Content -LiteralPath $InfFile.FullName -Raw -ErrorAction Stop) -match '(?im)^\s*Class\s*=\s*(SCSIAdapter|HDC)\s*(?:;.*)?$'
} catch {
& $Logger "Warning: could not classify storage driver '$($InfFile.FullName)': $_"
return $false
}
}
}
function Add-DriversToImage {
param ([string]$MountPath, [string]$DriverDir, [string]$Label = "image", [scriptblock]$Logger)
& dism /English "/image:$MountPath" /Add-Driver "/Driver:$DriverDir" /Recurse |
ForEach-Object { & $Logger " dism[$Label]: $_" }
}
function Invoke-WinUtilISODism {
param (
[Parameter(Mandatory)][string[]]$Arguments,
[Parameter(Mandatory)][string]$Operation
)
$output = @(& dism.exe @Arguments 2>&1)
$exitCode = $LASTEXITCODE
if ($exitCode -ne 0) {
foreach ($line in @($output | Select-Object -Last 20)) {
if (-not [string]::IsNullOrWhiteSpace([string]$line)) {
& $Logger " dism[$Operation]: $line"
}
}
throw "DISM $Operation failed with exit code $exitCode."
}
if ($Operation -ne 'metadata') {
& $Logger "DISM $Operation completed."
}
return $output
}
function Get-WinUtilISOWimMetadata {
param ([Parameter(Mandatory)][string]$ImagePath, [Parameter(Mandatory)][int]$Index)
$metadata = @{}
$output = Invoke-WinUtilISODism -Arguments @('/English', '/Get-WimInfo', "/WimFile:$ImagePath", "/Index:$Index") -Operation 'metadata'
foreach ($line in $output) {
if ([string]$line -match '^\s*([^:]+?)\s*:\s*(.*?)\s*$') {
$metadata[$Matches[1].Trim()] = $Matches[2].Trim()
}
}
return $metadata
}
function Assert-WinUtilISOWimMetadata {
param (
[Parameter(Mandatory)][hashtable]$Before,
[hashtable]$After
)
foreach ($key in 'Languages', 'Installation', 'Edition', 'ProductSuite', 'ProductType') {
$beforeValue = [string]$Before[$key]
if ($beforeValue -eq '<undefined>' -or ($key -in 'Installation', 'Edition', 'ProductType' -and [string]::IsNullOrWhiteSpace($beforeValue))) {
throw "install.wim metadata is already invalid: $key is undefined. Driver injection was not attempted."
}
if ($After) {
$afterValue = [string]$After[$key]
if ($afterValue -eq '<undefined>' -or ($beforeValue -and $afterValue -ne $beforeValue)) {
throw "install.wim metadata validation failed after driver injection: $key changed from '$beforeValue' to '$afterValue'."
}
}
}
}
function Test-WinUtilISOMountedImage {
param ([Parameter(Mandatory)][string]$Path)
return @(& dism.exe /English /Get-MountedImageInfo 2>$null) -match [regex]::Escape($Path)
}
if ([IO.Path]::GetExtension($InstallImagePath) -ne '.wim') {
throw 'Current-system driver injection requires install.wim; install.esd cannot be serviced in place.'
}
if (-not (Test-Path -LiteralPath $InstallImagePath)) {
throw "install.wim was not found: $InstallImagePath"
}
if ($InstallImageIndex -lt 1) {
throw 'Current-system driver injection requires a valid install.wim image index.'
}
$driverExportRoot = Join-Path $env:TEMP "WinUtil_DriverExport_$(Get-Date -Format 'yyyyMMdd_HHmmss')_$(([guid]::NewGuid()).ToString('N').Substring(0, 8))"
$mountDir = Join-Path (Split-Path -Path $ContentRoot -Parent) 'wim_mount'
New-Item -Path $driverExportRoot -ItemType Directory -Force | Out-Null
$imageMounted = $false
function Invoke-BootWimInject {
param ([string]$BootWimPath, [string]$DriverDir, [scriptblock]$Logger)
Set-ItemProperty -Path $BootWimPath -Name IsReadOnly -Value $false
$mountDir = Join-Path $env:TEMP "WinUtil_BootMount_$(Get-Random)"
New-Item -Path $mountDir -ItemType Directory -Force
try {
& $Logger "Mounting boot.wim (index 2) for driver injection..."
Mount-WindowsImage -ImagePath $BootWimPath -Index 2 -Path $mountDir
Add-DriversToImage -MountPath $mountDir -DriverDir $DriverDir -Label "boot" -Logger $Logger
& $Logger "Saving boot.wim..."
Dismount-WindowsImage -Path $mountDir -Save
& $Logger "boot.wim driver injection complete."
} catch {
& $Logger "Warning: boot.wim driver injection failed: $_"
try { Dismount-WindowsImage -Path $mountDir -Discard } catch { & $Logger "Warning: could not discard boot.wim mount: $_" }
& $Logger "Exporting current system drivers before modifying install.wim..."
$dismLog = Join-Path $env:TEMP "WinUtil_DismDriverExport_$(Get-Date -Format 'yyyyMMdd_HHmmss').log"
$dismProcess = Start-Process -FilePath "dism.exe" -ArgumentList "/online /export-driver /destination:`"$driverExportRoot`" /LogPath:`"$dismLog`"" -Wait -NoNewWindow -PassThru
if ($dismProcess.ExitCode -ne 0) {
throw "dism.exe driver export failed with exit code $($dismProcess.ExitCode)."
}
$driverInfs = @(Get-ChildItem -Path $driverExportRoot -Filter '*.inf' -Recurse -File)
if ($driverInfs.Count -eq 0) {
throw 'DISM exported no driver INF files.'
}
$driverFolders = @($driverInfs | Group-Object { $_.Directory.FullName })
$winpeDriverDir = Join-Path $ContentRoot '$WinpeDriver$'
$storageCount = 0
$copyFailures = 0
foreach ($driverFolderGroup in $driverFolders) {
$driverFolder = [string]$driverFolderGroup.Name
$storageInfs = @($driverFolderGroup.Group | Where-Object { Test-WinUtilISOStorageDriver -InfFile $_ })
if ($storageInfs.Count -eq 0) {
continue
}
try {
New-Item -Path $winpeDriverDir -ItemType Directory -Force | Out-Null
$winpeTarget = Copy-WinUtilISODriverFolder -Source $driverFolder -Destination $winpeDriverDir
$storageCount++
& $Logger "Staged boot-storage package '$driverFolder' for WinPE as '$winpeTarget'."
} catch {
$copyFailures++
& $Logger "Warning: failed to stage boot-storage package '$driverFolder': $_"
}
}
if ($copyFailures -gt 0) {
throw "Failed to stage $copyFailures boot-storage driver package folders."
}
& $Logger "Exported $($driverInfs.Count) driver INF files across $($driverFolders.Count) package folders; staged $storageCount boot-storage packages for WinPE."
$metadataBefore = Get-WinUtilISOWimMetadata -ImagePath $InstallImagePath -Index $InstallImageIndex
Assert-WinUtilISOWimMetadata -Before $metadataBefore
Set-ItemProperty -LiteralPath $InstallImagePath -Name IsReadOnly -Value $false
New-Item -Path $mountDir -ItemType Directory -Force | Out-Null
& $Logger "Mounting install.wim index $InstallImageIndex once for driver injection..."
Invoke-WinUtilISODism -Arguments @('/English', '/Mount-Image', "/ImageFile:$InstallImagePath", "/Index:$InstallImageIndex", "/MountDir:$mountDir") -Operation 'mount' | Out-Null
$imageMounted = $true
& $Logger "Adding all exported drivers to the selected Windows image in one DISM operation..."
Invoke-WinUtilISODism -Arguments @('/English', "/Image:$mountDir", '/Add-Driver', "/Driver:$driverExportRoot", '/Recurse') -Operation 'add-driver' | Out-Null
& $Logger 'Committing the driver-only install.wim change...'
Invoke-WinUtilISODism -Arguments @('/English', '/Unmount-Image', "/MountDir:$mountDir", '/Commit') -Operation 'commit' | Out-Null
$imageMounted = $false
$metadataAfter = Get-WinUtilISOWimMetadata -ImagePath $InstallImagePath -Index $InstallImageIndex
Assert-WinUtilISOWimMetadata -Before $metadataBefore -After $metadataAfter
& $Logger 'Driver injection complete; install.wim metadata validation passed.'
} finally {
Remove-Item -Path $mountDir -Recurse -Force
}
}
function Get-WinUtilISOScriptChildElement {
param (
[Parameter(Mandatory)][System.Xml.XmlElement]$Parent,
[Parameter(Mandatory)][string]$Name,
[Parameter(Mandatory)][string]$NamespaceUri
)
foreach ($childNode in $Parent.ChildNodes) {
if ($childNode.NodeType -eq [System.Xml.XmlNodeType]::Element -and
$childNode.LocalName -eq $Name -and
$childNode.NamespaceURI -eq $NamespaceUri) {
return [System.Xml.XmlElement]$childNode
if ($imageMounted -or (Test-WinUtilISOMountedImage -Path $mountDir)) {
try {
Invoke-WinUtilISODism -Arguments @('/English', '/Unmount-Image', "/MountDir:$mountDir", '/Discard') -Operation 'discard' | Out-Null
} catch {
& $Logger "Warning: could not discard the failed install.wim mount: $_"
}
}
Remove-Item -Path $mountDir -Recurse -Force -ErrorAction SilentlyContinue
Remove-Item -Path $driverExportRoot -Recurse -Force -ErrorAction SilentlyContinue
}
$childElement = $Parent.OwnerDocument.CreateElement($Name, $NamespaceUri)
[void]$Parent.AppendChild($childElement)
return $childElement
}
function ConvertTo-WinUtilISOAnswerFile {
param (
[Parameter(Mandatory)][string]$XmlContent,
[int]$ImageIndex = 1
)
if ($ImageIndex -lt 1) { $ImageIndex = 1 }
$unattendNs = "urn:schemas-microsoft-com:unattend"
$wcmNs = "http://schemas.microsoft.com/WMIConfig/2002/State"
$xmlDoc = [xml]::new()
$xmlDoc.PreserveWhitespace = $true
$xmlDoc.LoadXml($XmlContent)
if ($xmlDoc.DocumentElement.NamespaceURI -ne $unattendNs) {
throw "Unexpected autounattend.xml namespace: $($xmlDoc.DocumentElement.NamespaceURI)"
}
if (-not $xmlDoc.DocumentElement.HasAttribute("xmlns:wcm")) {
$xmlDoc.DocumentElement.SetAttribute("wcm", "http://www.w3.org/2000/xmlns/", $wcmNs)
}
$nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
$nsMgr.AddNamespace("u", $unattendNs)
$windowsPESettings = $xmlDoc.SelectSingleNode('/u:unattend/u:settings[@pass="windowsPE"]', $nsMgr)
if (-not $windowsPESettings) {
$windowsPESettings = $xmlDoc.CreateElement("settings", $unattendNs)
$windowsPESettings.SetAttribute("pass", "windowsPE")
[void]$xmlDoc.DocumentElement.PrependChild($windowsPESettings)
}
$setupComponent = $windowsPESettings.SelectSingleNode('u:component[@name="Microsoft-Windows-Setup"]', $nsMgr)
if (-not $setupComponent) {
$setupComponent = $xmlDoc.CreateElement("component", $unattendNs)
$setupComponent.SetAttribute("name", "Microsoft-Windows-Setup")
$setupComponent.SetAttribute("processorArchitecture", "amd64")
$setupComponent.SetAttribute("publicKeyToken", "31bf3856ad364e35")
$setupComponent.SetAttribute("language", "neutral")
$setupComponent.SetAttribute("versionScope", "nonSxS")
[void]$windowsPESettings.AppendChild($setupComponent)
}
$productKeyNodes = @($setupComponent.SelectNodes("u:UserData/u:ProductKey", $nsMgr))
foreach ($productKeyNode in $productKeyNodes) {
$keyNode = $productKeyNode.SelectSingleNode("u:Key", $nsMgr)
$keyValue = if ($keyNode) { $keyNode.InnerText.Trim() } else { "" }
if ([string]::IsNullOrWhiteSpace($keyValue) -or $keyValue -eq "00000-00000-00000-00000-00000") {
[void]$productKeyNode.ParentNode.RemoveChild($productKeyNode)
}
}
$imageInstall = Get-WinUtilISOScriptChildElement -Parent $setupComponent -Name "ImageInstall" -NamespaceUri $unattendNs
$osImage = Get-WinUtilISOScriptChildElement -Parent $imageInstall -Name "OSImage" -NamespaceUri $unattendNs
$installFrom = Get-WinUtilISOScriptChildElement -Parent $osImage -Name "InstallFrom" -NamespaceUri $unattendNs
$existingMetadataNodes = @($installFrom.SelectNodes("u:MetaData", $nsMgr))
foreach ($metadataNode in $existingMetadataNodes) {
[void]$installFrom.RemoveChild($metadataNode)
}
$metadata = $xmlDoc.CreateElement("MetaData", $unattendNs)
$actionAttribute = $xmlDoc.CreateAttribute("wcm", "action", $wcmNs)
$actionAttribute.Value = "add"
[void]$metadata.Attributes.Append($actionAttribute)
$keyElement = $xmlDoc.CreateElement("Key", $unattendNs)
$keyElement.InnerText = "/IMAGE/INDEX"
[void]$metadata.AppendChild($keyElement)
$valueElement = $xmlDoc.CreateElement("Value", $unattendNs)
$valueElement.InnerText = [string]$ImageIndex
[void]$metadata.AppendChild($valueElement)
[void]$installFrom.AppendChild($metadata)
return $xmlDoc.OuterXml
}
function Write-WinUtilISOEditionConfig {
@@ -224,10 +232,6 @@ function Invoke-WinUtilISOScript {
[scriptblock]$Logger
)
if (-not (Test-Path $ContentRoot)) {
return
}
$sourcesDir = Join-Path $ContentRoot "sources"
New-Item -Path $sourcesDir -ItemType Directory -Force | Out-Null
@@ -256,253 +260,281 @@ Retail
& $Logger "Written sources\ei.cfg for EditionID '$EditionId'."
}
# -- 1. Remove provisioned AppX packages ----------------------------------
& $Log "Removing provisioned AppX packages..."
function Add-WinUtilISOSetupCustomizations {
param (
[Parameter(Mandatory)][string]$XmlContent,
[Parameter(Mandatory)][int]$InstallImageIndex,
[scriptblock]$Logger
)
$packages = & dism /English "/image:$ScratchDir" /Get-ProvisionedAppxPackages |
ForEach-Object { if ($_ -match 'PackageName : (.*)') { $matches[1] } }
$appxPackages = @(
'Clipchamp.Clipchamp', 'Microsoft.BingNews', 'Microsoft.BingSearch',
'Microsoft.BingWeather', 'Microsoft.GetHelp', 'Microsoft.MicrosoftOfficeHub',
'Microsoft.MicrosoftSolitaireCollection', 'Microsoft.MicrosoftStickyNotes',
'Microsoft.OutlookForWindows', 'Microsoft.Paint', 'Microsoft.PowerAutomateDesktop',
'Microsoft.StartExperiencesApp', 'Microsoft.Todos', 'Microsoft.Windows.DevHome',
'Microsoft.WindowsFeedbackHub', 'Microsoft.WindowsSoundRecorder',
'Microsoft.ZuneMusic', 'MicrosoftCorporationII.QuickAssist', 'MSTeams'
)
$packagePrefixes = @(
'Clipchamp.Clipchamp',
'Microsoft.BingNews',
'Microsoft.BingSearch',
'Microsoft.BingWeather',
'Microsoft.GetHelp',
'Microsoft.MicrosoftOfficeHub',
'Microsoft.MicrosoftSolitaireCollection',
'Microsoft.MicrosoftStickyNotes',
'Microsoft.OutlookForWindows',
'Microsoft.Paint',
'Microsoft.PowerAutomateDesktop',
'Microsoft.StartExperiencesApp',
'Microsoft.Todos',
'Microsoft.Windows.DevHome',
'Microsoft.WindowsFeedbackHub',
'Microsoft.WindowsSoundRecorder',
'Microsoft.ZuneMusic',
'MicrosoftCorporationII.QuickAssist',
'MSTeams'
$appxList = ($appxPackages | ForEach-Object { " '$_'" }) -join "`r`n"
$postInstallScript = @"
`$ErrorActionPreference = 'Continue'
`$logPath = 'C:\Windows\Setup\Scripts\WinUtil-PostInstall.log'
Start-Transcript -Path `$logPath -Append -ErrorAction SilentlyContinue
try {
Write-Host 'WinUtil: Removing provisioned AppX packages...'
`$packages = @(
$appxList
)
foreach (`$package in `$packages) {
Get-AppxProvisionedPackage -Online -ErrorAction SilentlyContinue |
Where-Object { `$_.DisplayName -like "*`$package*" } |
ForEach-Object { Remove-AppxProvisionedPackage -Online -PackageName `$_.PackageName -ErrorAction SilentlyContinue | Out-Null }
Get-AppxPackage -AllUsers -ErrorAction SilentlyContinue |
Where-Object { `$_.Name -like "*`$package*" } |
ForEach-Object { Remove-AppxPackage -AllUsers -Package `$_.PackageFullName -ErrorAction SilentlyContinue | Out-Null }
}
$packages | Where-Object { $pkg = $_; $packagePrefixes | Where-Object { $pkg -like "*$_*" } } |
ForEach-Object { & dism /English "/image:$ScratchDir" /Remove-ProvisionedAppxPackage "/PackageName:$_" }
function Set-WinUtilRegistryValue([string]`$Path, [string]`$Name, [string]`$Type, [string]`$Value) {
reg.exe add `$Path /v `$Name /t `$Type /d `$Value /f 2>&1 | Out-Null
}
function Set-WinUtilContentDeliveryManagerValues([string]`$HiveRoot) {
`$contentDeliveryManager = "`$HiveRoot\SOFTWARE\Microsoft\Windows\CurrentVersion\ContentDeliveryManager"
Set-WinUtilRegistryValue `$contentDeliveryManager 'OemPreInstalledAppsEnabled' 'REG_DWORD' '0'
Set-WinUtilRegistryValue `$contentDeliveryManager 'PreInstalledAppsEnabled' 'REG_DWORD' '0'
Set-WinUtilRegistryValue `$contentDeliveryManager 'SilentInstalledAppsEnabled' 'REG_DWORD' '0'
Set-WinUtilRegistryValue `$contentDeliveryManager 'ContentDeliveryAllowed' 'REG_DWORD' '0'
Set-WinUtilRegistryValue `$contentDeliveryManager 'FeatureManagementEnabled' 'REG_DWORD' '0'
Set-WinUtilRegistryValue `$contentDeliveryManager 'PreInstalledAppsEverEnabled' 'REG_DWORD' '0'
Set-WinUtilRegistryValue `$contentDeliveryManager 'SoftLandingEnabled' 'REG_DWORD' '0'
Set-WinUtilRegistryValue `$contentDeliveryManager 'SubscribedContentEnabled' 'REG_DWORD' '0'
Set-WinUtilRegistryValue `$contentDeliveryManager 'SubscribedContent-310093Enabled' 'REG_DWORD' '0'
Set-WinUtilRegistryValue `$contentDeliveryManager 'SubscribedContent-338388Enabled' 'REG_DWORD' '0'
Set-WinUtilRegistryValue `$contentDeliveryManager 'SubscribedContent-338389Enabled' 'REG_DWORD' '0'
Set-WinUtilRegistryValue `$contentDeliveryManager 'SubscribedContent-338393Enabled' 'REG_DWORD' '0'
Set-WinUtilRegistryValue `$contentDeliveryManager 'SubscribedContent-353694Enabled' 'REG_DWORD' '0'
Set-WinUtilRegistryValue `$contentDeliveryManager 'SubscribedContent-353696Enabled' 'REG_DWORD' '0'
Set-WinUtilRegistryValue `$contentDeliveryManager 'SystemPaneSuggestionsEnabled' 'REG_DWORD' '0'
reg.exe delete "`$contentDeliveryManager\Subscriptions" /f 2>&1 | Out-Null
reg.exe delete "`$contentDeliveryManager\SuggestedApps" /f 2>&1 | Out-Null
}
Write-Host 'WinUtil: Applying registry tweaks...'
Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\ReserveManager' 'ShippedWithReserves' 'REG_DWORD' '0'
Set-WinUtilRegistryValue 'HKLM\SYSTEM\CurrentControlSet\Control\BitLocker' 'PreventDeviceEncryption' 'REG_DWORD' '1'
Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Policies\Microsoft\Windows\Windows Chat' 'ChatIcon' 'REG_DWORD' '3'
Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Policies\Microsoft\Windows\OneDrive' 'DisableFileSyncNGSC' 'REG_DWORD' '1'
Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Policies\Microsoft\Windows\DataCollection' 'AllowTelemetry' 'REG_DWORD' '0'
Set-WinUtilRegistryValue 'HKLM\SYSTEM\CurrentControlSet\Services\dmwappushservice' 'Start' 'REG_DWORD' '4'
Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsCopilot' 'TurnOffWindowsCopilot' 'REG_DWORD' '1'
Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Policies\Microsoft\Edge' 'HubsSidebarEnabled' 'REG_DWORD' '0'
Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Policies\Microsoft\Windows\Explorer' 'DisableSearchBoxSuggestions' 'REG_DWORD' '1'
Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Policies\Microsoft\Teams' 'DisableInstallation' 'REG_DWORD' '1'
Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Policies\Microsoft\Windows\Windows Mail' 'PreventRun' 'REG_DWORD' '1'
Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Policies\Microsoft\Windows\CloudContent' 'DisableWindowsConsumerFeatures' 'REG_DWORD' '1'
Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Policies\Microsoft\Windows\CloudContent' 'DisableConsumerAccountStateContent' 'REG_DWORD' '1'
Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Policies\Microsoft\Windows\CloudContent' 'DisableCloudOptimizedContent' 'REG_DWORD' '1'
Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Microsoft\PolicyManager\current\device\Start' 'ConfigureStartPins' 'REG_SZ' '{"pinnedList": [{}]}'
Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE' 'BypassNRO' 'REG_DWORD' '1'
Set-WinUtilRegistryValue 'HKLM\SYSTEM\Setup\LabConfig' 'BypassCPUCheck' 'REG_DWORD' '1'
Set-WinUtilRegistryValue 'HKLM\SYSTEM\Setup\LabConfig' 'BypassRAMCheck' 'REG_DWORD' '1'
Set-WinUtilRegistryValue 'HKLM\SYSTEM\Setup\LabConfig' 'BypassSecureBootCheck' 'REG_DWORD' '1'
Set-WinUtilRegistryValue 'HKLM\SYSTEM\Setup\LabConfig' 'BypassStorageCheck' 'REG_DWORD' '1'
Set-WinUtilRegistryValue 'HKLM\SYSTEM\Setup\LabConfig' 'BypassTPMCheck' 'REG_DWORD' '1'
Set-WinUtilRegistryValue 'HKLM\SYSTEM\Setup\MoSetup' 'AllowUpgradesWithUnsupportedTPMOrCPU' 'REG_DWORD' '1'
Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Policies\Microsoft\PushToInstall' 'DisablePushToInstall' 'REG_DWORD' '1'
Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Policies\Microsoft\MRT' 'DontOfferThroughWUAU' 'REG_DWORD' '1'
Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Orchestrator\UScheduler_Oobe\OutlookUpdate' 'workCompleted' 'REG_DWORD' '1'
Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Orchestrator\UScheduler\OutlookUpdate' 'workCompleted' 'REG_DWORD' '1'
Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Orchestrator\UScheduler\DevHomeUpdate' 'workCompleted' 'REG_DWORD' '1'
reg.exe delete 'HKLM\SOFTWARE\Microsoft\WindowsUpdate\Orchestrator\UScheduler_Oobe\OutlookUpdate' /f 2>&1 | Out-Null
reg.exe delete 'HKLM\SOFTWARE\Microsoft\WindowsUpdate\Orchestrator\UScheduler_Oobe\DevHomeUpdate' /f 2>&1 | Out-Null
Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU' 'NoAutoUpdate' 'REG_DWORD' '1'
Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU' 'AUOptions' 'REG_DWORD' '1'
Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU' 'UseWUServer' 'REG_DWORD' '1'
Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate' 'DisableWindowsUpdateAccess' 'REG_DWORD' '1'
Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate' 'WUServer' 'REG_SZ' 'http://localhost:8080'
Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate' 'WUStatusServer' 'REG_SZ' 'http://localhost:8080'
Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Orchestrator\UScheduler_Oobe\WindowsUpdate' 'workCompleted' 'REG_DWORD' '1'
reg.exe delete 'HKLM\SOFTWARE\Microsoft\WindowsUpdate\Orchestrator\UScheduler_Oobe\WindowsUpdate' /f 2>&1 | Out-Null
Set-WinUtilRegistryValue 'HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\DeliveryOptimization\Config' 'DODownloadMode' 'REG_DWORD' '0'
Set-WinUtilRegistryValue 'HKLM\SYSTEM\CurrentControlSet\Services\BITS' 'Start' 'REG_DWORD' '4'
Set-WinUtilRegistryValue 'HKLM\SYSTEM\CurrentControlSet\Services\wuauserv' 'Start' 'REG_DWORD' '4'
Set-WinUtilRegistryValue 'HKLM\SYSTEM\CurrentControlSet\Services\UsoSvc' 'Start' 'REG_DWORD' '4'
Set-WinUtilRegistryValue 'HKLM\SYSTEM\CurrentControlSet\Services\WaaSMedicSvc' 'Start' 'REG_DWORD' '4'
`$defaultHive = 'HKU\WinUtilDefault'
reg.exe load `$defaultHive 'C:\Users\Default\NTUSER.DAT' 2>&1 | Out-Null
if (`$LASTEXITCODE -eq 0) {
Set-WinUtilRegistryValue "`$defaultHive\Control Panel\UnsupportedHardwareNotificationCache" 'SV1' 'REG_DWORD' '0'
Set-WinUtilRegistryValue "`$defaultHive\Control Panel\UnsupportedHardwareNotificationCache" 'SV2' 'REG_DWORD' '0'
Set-WinUtilContentDeliveryManagerValues `$defaultHive
Set-WinUtilRegistryValue "`$defaultHive\Software\Microsoft\Windows\CurrentVersion\AdvertisingInfo" 'Enabled' 'REG_DWORD' '0'
Set-WinUtilRegistryValue "`$defaultHive\Software\Microsoft\Windows\CurrentVersion\Privacy" 'TailoredExperiencesWithDiagnosticDataEnabled' 'REG_DWORD' '0'
Set-WinUtilRegistryValue "`$defaultHive\Software\Microsoft\Speech_OneCore\Settings\OnlineSpeechPrivacy" 'HasAccepted' 'REG_DWORD' '0'
Set-WinUtilRegistryValue "`$defaultHive\Software\Microsoft\Input\TIPC" 'Enabled' 'REG_DWORD' '0'
Set-WinUtilRegistryValue "`$defaultHive\Software\Microsoft\InputPersonalization" 'RestrictImplicitInkCollection' 'REG_DWORD' '1'
Set-WinUtilRegistryValue "`$defaultHive\Software\Microsoft\InputPersonalization" 'RestrictImplicitTextCollection' 'REG_DWORD' '1'
Set-WinUtilRegistryValue "`$defaultHive\Software\Microsoft\InputPersonalization\TrainedDataStore" 'HarvestContacts' 'REG_DWORD' '0'
Set-WinUtilRegistryValue "`$defaultHive\Software\Microsoft\Personalization\Settings" 'AcceptedPrivacyPolicy' 'REG_DWORD' '0'
reg.exe unload `$defaultHive 2>&1 | Out-Null
}
Set-WinUtilContentDeliveryManagerValues 'HKCU'
Set-WinUtilRegistryValue 'HKCU\Control Panel\UnsupportedHardwareNotificationCache' 'SV1' 'REG_DWORD' '0'
Set-WinUtilRegistryValue 'HKCU\Control Panel\UnsupportedHardwareNotificationCache' 'SV2' 'REG_DWORD' '0'
Set-WinUtilRegistryValue 'HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced' 'TaskbarMn' 'REG_DWORD' '0'
Set-WinUtilRegistryValue 'HKCU\Software\Microsoft\Windows\CurrentVersion\AdvertisingInfo' 'Enabled' 'REG_DWORD' '0'
Set-WinUtilRegistryValue 'HKCU\Software\Microsoft\Windows\CurrentVersion\Privacy' 'TailoredExperiencesWithDiagnosticDataEnabled' 'REG_DWORD' '0'
Set-WinUtilRegistryValue 'HKCU\Software\Microsoft\Speech_OneCore\Settings\OnlineSpeechPrivacy' 'HasAccepted' 'REG_DWORD' '0'
Set-WinUtilRegistryValue 'HKCU\Software\Microsoft\Input\TIPC' 'Enabled' 'REG_DWORD' '0'
Set-WinUtilRegistryValue 'HKCU\Software\Microsoft\InputPersonalization' 'RestrictImplicitInkCollection' 'REG_DWORD' '1'
Set-WinUtilRegistryValue 'HKCU\Software\Microsoft\InputPersonalization' 'RestrictImplicitTextCollection' 'REG_DWORD' '1'
Set-WinUtilRegistryValue 'HKCU\Software\Microsoft\InputPersonalization\TrainedDataStore' 'HarvestContacts' 'REG_DWORD' '0'
Set-WinUtilRegistryValue 'HKCU\Software\Microsoft\Personalization\Settings' 'AcceptedPrivacyPolicy' 'REG_DWORD' '0'
Write-Host 'WinUtil: Removing scheduled task definitions...'
`$taskPaths = @(
'C:\Windows\System32\Tasks\Microsoft\Windows\Application Experience\Microsoft Compatibility Appraiser',
'C:\Windows\System32\Tasks\Microsoft\Windows\Customer Experience Improvement Program',
'C:\Windows\System32\Tasks\Microsoft\Windows\Application Experience\ProgramDataUpdater',
'C:\Windows\System32\Tasks\Microsoft\Windows\Chkdsk\Proxy',
'C:\Windows\System32\Tasks\Microsoft\Windows\Windows Error Reporting\QueueReporting',
'C:\Windows\System32\Tasks\Microsoft\Windows\InstallService',
'C:\Windows\System32\Tasks\Microsoft\Windows\UpdateOrchestrator',
'C:\Windows\System32\Tasks\Microsoft\Windows\UpdateAssistant',
'C:\Windows\System32\Tasks\Microsoft\Windows\WaaSMedic',
'C:\Windows\System32\Tasks\Microsoft\Windows\WindowsUpdate',
'C:\Windows\System32\Tasks\Microsoft\WindowsUpdate'
)
foreach (`$taskPath in `$taskPaths) { Remove-Item -LiteralPath `$taskPath -Recurse -Force -ErrorAction SilentlyContinue }
Start-Process -FilePath 'C:\Windows\System32\OneDriveSetup.exe' -ArgumentList '/uninstall' -Wait -ErrorAction SilentlyContinue
Write-Host 'WinUtil: Post-install customization complete.'
} finally {
Stop-Transcript -ErrorAction SilentlyContinue
}
"@
$xmlDoc = [xml]::new()
$xmlDoc.PreserveWhitespace = $true
$xmlDoc.LoadXml($XmlContent)
$nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
$nsMgr.AddNamespace('u', 'urn:schemas-microsoft-com:unattend')
$nsMgr.AddNamespace('sg', 'https://schneegans.de/windows/unattend-generator/')
$setupComponent = $xmlDoc.SelectSingleNode('/u:unattend/u:settings[@pass="windowsPE"]/u:component[@name="Microsoft-Windows-Setup"]', $nsMgr)
$extensions = $xmlDoc.SelectSingleNode('//sg:Extensions', $nsMgr)
$firstLogonFile = $xmlDoc.SelectSingleNode('//sg:File[@path="C:\Windows\Setup\Scripts\FirstLogon.ps1"]', $nsMgr)
if (-not $setupComponent -or -not $extensions -or -not $firstLogonFile) {
throw 'autounattend.xml is missing a required Windows Setup, Extensions, or FirstLogon.ps1 node.'
}
$imageInstall = $setupComponent.SelectSingleNode('u:ImageInstall', $nsMgr)
if (-not $imageInstall) {
$imageInstall = $xmlDoc.CreateElement('ImageInstall', $setupComponent.NamespaceURI)
[void]$setupComponent.AppendChild($imageInstall)
}
$osImage = $imageInstall.SelectSingleNode('u:OSImage', $nsMgr)
if (-not $osImage) {
$osImage = $xmlDoc.CreateElement('OSImage', $setupComponent.NamespaceURI)
[void]$imageInstall.AppendChild($osImage)
}
$installFrom = $osImage.SelectSingleNode('u:InstallFrom', $nsMgr)
if (-not $installFrom) {
$installFrom = $xmlDoc.CreateElement('InstallFrom', $setupComponent.NamespaceURI)
[void]$osImage.AppendChild($installFrom)
}
foreach ($existingMetadata in @($installFrom.SelectNodes('u:MetaData', $nsMgr))) {
[void]$installFrom.RemoveChild($existingMetadata)
}
$metadata = $xmlDoc.CreateElement('MetaData', $setupComponent.NamespaceURI)
$action = $xmlDoc.CreateAttribute('wcm', 'action', 'http://schemas.microsoft.com/WMIConfig/2002/State')
$action.Value = 'add'
[void]$metadata.Attributes.Append($action)
$key = $xmlDoc.CreateElement('Key', $setupComponent.NamespaceURI)
$key.InnerText = '/IMAGE/INDEX'
[void]$metadata.AppendChild($key)
$value = $xmlDoc.CreateElement('Value', $setupComponent.NamespaceURI)
$value.InnerText = [string]$InstallImageIndex
[void]$metadata.AppendChild($value)
[void]$installFrom.AppendChild($metadata)
$postInstallFile = $xmlDoc.CreateElement('File', $extensions.NamespaceURI)
$postInstallFile.SetAttribute('path', 'C:\Windows\Setup\Scripts\WinUtil-PostInstall.ps1')
$postInstallFile.InnerText = $postInstallScript
[void]$extensions.AppendChild($postInstallFile)
$firstLogonFile.InnerText = "& 'C:\Windows\Setup\Scripts\WinUtil-PostInstall.ps1';`r`n`r`n$($firstLogonFile.InnerText.Trim())"
$null = & $Logger 'Added WinUtil post-install AppX, registry, and scheduled-task customizations to autounattend.xml.'
return $xmlDoc.OuterXml
}
function Add-WinUtilISOSetupScriptFallback {
param (
[Parameter(Mandatory)][string]$ContentRoot,
[Parameter(Mandatory)][string]$XmlContent,
[scriptblock]$Logger
)
$xmlDoc = [xml]::new()
$xmlDoc.PreserveWhitespace = $true
$xmlDoc.LoadXml($XmlContent)
$nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
$nsMgr.AddNamespace('u', 'urn:schemas-microsoft-com:unattend')
$nsMgr.AddNamespace('sg', 'https://schneegans.de/windows/unattend-generator/')
$setupScriptsRoot = Join-Path $ContentRoot 'sources\$OEM$\$$\Setup\Scripts'
$stagedCount = 0
foreach ($file in $xmlDoc.SelectNodes('//sg:File', $nsMgr)) {
$path = $file.GetAttribute('path')
if (-not $path.StartsWith('C:\Windows\Setup\Scripts\', [System.StringComparison]::OrdinalIgnoreCase)) {
continue
}
$relativePath = $path.Substring('C:\Windows\Setup\Scripts\'.Length)
$targetPath = Join-Path $setupScriptsRoot $relativePath
New-Item -Path (Split-Path $targetPath -Parent) -ItemType Directory -Force | Out-Null
$encoding = switch ([System.IO.Path]::GetExtension($targetPath)) {
{ $_ -in '.ps1', '.xml' } { [System.Text.Encoding]::UTF8; break }
{ $_ -in '.reg', '.vbs', '.js' } { [System.Text.UnicodeEncoding]::new($false, $true); break }
default { [System.Text.Encoding]::Default }
}
$bytes = $encoding.GetPreamble() + $encoding.GetBytes($file.InnerText.Trim())
[System.IO.File]::WriteAllBytes($targetPath, $bytes)
$stagedCount++
}
$useConfigurationSet = $xmlDoc.SelectSingleNode('/u:unattend/u:settings[@pass="windowsPE"]/u:component[@name="Microsoft-Windows-Setup"]/u:UseConfigurationSet', $nsMgr)
if ($useConfigurationSet) {
$useConfigurationSet.InnerText = 'true'
[System.IO.File]::WriteAllText((Join-Path $ContentRoot 'autounattend.xml'), $xmlDoc.OuterXml, [System.Text.UTF8Encoding]::new($false))
}
& $Logger "Staged $stagedCount WinUtil setup script fallback files at '$setupScriptsRoot'."
}
if (-not (Test-Path $ISOContentsDir)) {
throw "ISO contents directory does not exist: $ISOContentsDir"
}
if ([string]::IsNullOrWhiteSpace($AutoUnattendXml)) {
throw "autounattend.xml content is required to prepare setup media."
}
$preparedAutoUnattendXml = Add-WinUtilISOSetupCustomizations -XmlContent $AutoUnattendXml -InstallImageIndex $InstallImageIndex -Logger $Log
$unattendPath = Join-Path $ISOContentsDir "autounattend.xml"
[System.IO.File]::WriteAllText($unattendPath, $preparedAutoUnattendXml, [System.Text.UTF8Encoding]::new($false))
& $Log "Written autounattend.xml with WinUtil setup customizations to ISO root ($unattendPath)."
Add-WinUtilISOSetupScriptFallback -ContentRoot $ISOContentsDir -XmlContent $preparedAutoUnattendXml -Logger $Log
Write-WinUtilISOEditionConfig -ContentRoot $ISOContentsDir -EditionId $InstallEditionId -Logger $Log
# -- 2. Inject current system drivers (optional) ---------------------------
if ($InjectCurrentSystemDrivers) {
& $Log "Exporting all drivers from running system..."
$driverExportRoot = Join-Path $env:TEMP "WinUtil_DriverExport_$(Get-Random)"
New-Item -Path $driverExportRoot -ItemType Directory -Force
try {
Export-WindowsDriver -Online -Destination $driverExportRoot
& $Log "Injecting current system drivers into install.wim..."
Add-DriversToImage -MountPath $ScratchDir -DriverDir $driverExportRoot -Label "install" -Logger $Log
& $Log "install.wim driver injection complete."
if ($ISOContentsDir -and (Test-Path $ISOContentsDir)) {
$bootWim = Join-Path $ISOContentsDir "sources\boot.wim"
if (Test-Path $bootWim) {
& $Log "Injecting current system drivers into boot.wim..."
Invoke-BootWimInject -BootWimPath $bootWim -DriverDir $driverExportRoot -Logger $Log
} else {
& $Log "Warning: boot.wim not found - skipping boot.wim driver injection."
}
}
} catch {
& $Log "Error during driver export/injection: $_"
} finally {
Remove-Item -Path $driverExportRoot -Recurse -Force
}
} else {
& $Log "Driver injection skipped."
}
# -- 3. Registry tweaks ----------------------------------------------------
& $Log "Loading offline registry hives..."
reg load HKLM\zCOMPONENTS "$ScratchDir\Windows\System32\config\COMPONENTS"
reg load HKLM\zDEFAULT "$ScratchDir\Windows\System32\config\default"
reg load HKLM\zNTUSER "$ScratchDir\Users\Default\ntuser.dat"
reg load HKLM\zSOFTWARE "$ScratchDir\Windows\System32\config\SOFTWARE"
reg load HKLM\zSYSTEM "$ScratchDir\Windows\System32\config\SYSTEM"
& $Log "Bypassing system requirements..."
Set-ISOScriptReg -Path 'HKLM\zDEFAULT\Control Panel\UnsupportedHardwareNotificationCache' -Name 'SV1' -Type 'REG_DWORD' -Value '0'
Set-ISOScriptReg -Path 'HKLM\zDEFAULT\Control Panel\UnsupportedHardwareNotificationCache' -Name 'SV2' -Type 'REG_DWORD' -Value '0'
Set-ISOScriptReg -Path 'HKLM\zNTUSER\Control Panel\UnsupportedHardwareNotificationCache' -Name 'SV1' -Type 'REG_DWORD' -Value '0'
Set-ISOScriptReg -Path 'HKLM\zNTUSER\Control Panel\UnsupportedHardwareNotificationCache' -Name 'SV2' -Type 'REG_DWORD' -Value '0'
Set-ISOScriptReg -Path 'HKLM\zSYSTEM\Setup\LabConfig' -Name 'BypassCPUCheck' -Type 'REG_DWORD' -Value '1'
Set-ISOScriptReg -Path 'HKLM\zSYSTEM\Setup\LabConfig' -Name 'BypassRAMCheck' -Type 'REG_DWORD' -Value '1'
Set-ISOScriptReg -Path 'HKLM\zSYSTEM\Setup\LabConfig' -Name 'BypassSecureBootCheck' -Type 'REG_DWORD' -Value '1'
Set-ISOScriptReg -Path 'HKLM\zSYSTEM\Setup\LabConfig' -Name 'BypassStorageCheck' -Type 'REG_DWORD' -Value '1'
Set-ISOScriptReg -Path 'HKLM\zSYSTEM\Setup\LabConfig' -Name 'BypassTPMCheck' -Type 'REG_DWORD' -Value '1'
Set-ISOScriptReg -Path 'HKLM\zSYSTEM\Setup\MoSetup' -Name 'AllowUpgradesWithUnsupportedTPMOrCPU' -Type 'REG_DWORD' -Value '1'
& $Log "Disabling sponsored apps..."
Set-ISOScriptReg -Path 'HKLM\zNTUSER\SOFTWARE\Microsoft\Windows\CurrentVersion\ContentDeliveryManager' -Name 'OemPreInstalledAppsEnabled' -Type 'REG_DWORD' -Value '0'
Set-ISOScriptReg -Path 'HKLM\zNTUSER\SOFTWARE\Microsoft\Windows\CurrentVersion\ContentDeliveryManager' -Name 'PreInstalledAppsEnabled' -Type 'REG_DWORD' -Value '0'
Set-ISOScriptReg -Path 'HKLM\zNTUSER\SOFTWARE\Microsoft\Windows\CurrentVersion\ContentDeliveryManager' -Name 'SilentInstalledAppsEnabled' -Type 'REG_DWORD' -Value '0'
Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Policies\Microsoft\Windows\CloudContent' -Name 'DisableWindowsConsumerFeatures' -Type 'REG_DWORD' -Value '1'
Set-ISOScriptReg -Path 'HKLM\zNTUSER\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager' -Name 'ContentDeliveryAllowed' -Type 'REG_DWORD' -Value '0'
Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Microsoft\PolicyManager\current\device\Start' -Name 'ConfigureStartPins' -Type 'REG_SZ' -Value '{"pinnedList": [{}]}'
Set-ISOScriptReg -Path 'HKLM\zNTUSER\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager' -Name 'FeatureManagementEnabled' -Type 'REG_DWORD' -Value '0'
Set-ISOScriptReg -Path 'HKLM\zNTUSER\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager' -Name 'PreInstalledAppsEverEnabled' -Type 'REG_DWORD' -Value '0'
Set-ISOScriptReg -Path 'HKLM\zNTUSER\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager' -Name 'SoftLandingEnabled' -Type 'REG_DWORD' -Value '0'
Set-ISOScriptReg -Path 'HKLM\zNTUSER\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager' -Name 'SubscribedContentEnabled' -Type 'REG_DWORD' -Value '0'
Set-ISOScriptReg -Path 'HKLM\zNTUSER\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager' -Name 'SubscribedContent-310093Enabled' -Type 'REG_DWORD' -Value '0'
Set-ISOScriptReg -Path 'HKLM\zNTUSER\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager' -Name 'SubscribedContent-338388Enabled' -Type 'REG_DWORD' -Value '0'
Set-ISOScriptReg -Path 'HKLM\zNTUSER\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager' -Name 'SubscribedContent-338389Enabled' -Type 'REG_DWORD' -Value '0'
Set-ISOScriptReg -Path 'HKLM\zNTUSER\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager' -Name 'SubscribedContent-338393Enabled' -Type 'REG_DWORD' -Value '0'
Set-ISOScriptReg -Path 'HKLM\zNTUSER\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager' -Name 'SubscribedContent-353694Enabled' -Type 'REG_DWORD' -Value '0'
Set-ISOScriptReg -Path 'HKLM\zNTUSER\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager' -Name 'SubscribedContent-353696Enabled' -Type 'REG_DWORD' -Value '0'
Set-ISOScriptReg -Path 'HKLM\zNTUSER\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager' -Name 'SystemPaneSuggestionsEnabled' -Type 'REG_DWORD' -Value '0'
Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Policies\Microsoft\PushToInstall' -Name 'DisablePushToInstall' -Type 'REG_DWORD' -Value '1'
Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Policies\Microsoft\MRT' -Name 'DontOfferThroughWUAU' -Type 'REG_DWORD' -Value '1'
Remove-ISOScriptReg 'HKLM\zNTUSER\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager\Subscriptions'
Remove-ISOScriptReg 'HKLM\zNTUSER\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager\SuggestedApps'
Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Policies\Microsoft\Windows\CloudContent' -Name 'DisableConsumerAccountStateContent' -Type 'REG_DWORD' -Value '1'
Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Policies\Microsoft\Windows\CloudContent' -Name 'DisableCloudOptimizedContent' -Type 'REG_DWORD' -Value '1'
& $Log "Enabling local accounts on OOBE..."
Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Microsoft\Windows\CurrentVersion\OOBE' -Name 'BypassNRO' -Type 'REG_DWORD' -Value '1'
if ($AutoUnattendXml) {
$preparedAutoUnattendXml = $AutoUnattendXml
try {
$preparedAutoUnattendXml = ConvertTo-WinUtilISOAnswerFile -XmlContent $AutoUnattendXml -ImageIndex $InstallImageIndex
& $Log "Prepared autounattend.xml to install image index $InstallImageIndex without forcing a product key."
} catch {
& $Log "Warning: could not prepare autounattend.xml image selection: $_"
}
try {
$xmlDoc = [xml]::new()
$xmlDoc.LoadXml($preparedAutoUnattendXml)
$nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
$nsMgr.AddNamespace("sg", "https://schneegans.de/windows/unattend-generator/")
$fileNodes = $xmlDoc.SelectNodes("//sg:File", $nsMgr)
if ($fileNodes -and $fileNodes.Count -gt 0) {
foreach ($fileNode in $fileNodes) {
$absPath = $fileNode.GetAttribute("path")
$relPath = $absPath -replace '^[A-Za-z]:[/\\]', ''
$destPath = Join-Path $ScratchDir $relPath
New-Item -Path (Split-Path $destPath -Parent) -ItemType Directory -Force
$ext = [IO.Path]::GetExtension($destPath).ToLower()
$encoding = switch ($ext) {
{ $_ -in '.ps1', '.xml' } { [System.Text.Encoding]::UTF8 }
{ $_ -in '.reg', '.vbs', '.js' } { [System.Text.UnicodeEncoding]::new($false, $true) }
default { [System.Text.Encoding]::Default }
}
[System.IO.File]::WriteAllBytes($destPath, ($encoding.GetPreamble() + $encoding.GetBytes($fileNode.InnerText.Trim())))
& $Log "Pre-staged setup script: $relPath"
}
} else {
& $Log "Warning: no <Extensions><File> nodes found in autounattend.xml - setup scripts not pre-staged."
}
} catch {
& $Log "Warning: could not pre-stage setup scripts from autounattend.xml: $_"
}
if ($ISOContentsDir -and (Test-Path $ISOContentsDir)) {
$isoDest = Join-Path $ISOContentsDir "autounattend.xml"
Set-Content -Path $isoDest -Value $preparedAutoUnattendXml -Encoding UTF8 -Force
& $Log "Written autounattend.xml to ISO root ($isoDest)."
}
} else {
& $Log "Warning: autounattend.xml content is empty - skipping OOBE bypass file."
}
if ($ISOContentsDir -and (Test-Path $ISOContentsDir)) {
Write-WinUtilISOEditionConfig -ContentRoot $ISOContentsDir -EditionId $InstallEditionId -Logger $Log
}
& $Log "Disabling reserved storage..."
Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Microsoft\Windows\CurrentVersion\ReserveManager' -Name 'ShippedWithReserves' -Type 'REG_DWORD' -Value '0'
& $Log "Disabling BitLocker device encryption..."
Set-ISOScriptReg -Path 'HKLM\zSYSTEM\ControlSet001\Control\BitLocker' -Name 'PreventDeviceEncryption' -Type 'REG_DWORD' -Value '1'
& $Log "Disabling Chat icon..."
Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Policies\Microsoft\Windows\Windows Chat' -Name 'ChatIcon' -Type 'REG_DWORD' -Value '3'
Set-ISOScriptReg -Path 'HKLM\zNTUSER\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced' -Name 'TaskbarMn' -Type 'REG_DWORD' -Value '0'
& $Log "Disabling OneDrive folder backup..."
Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Policies\Microsoft\Windows\OneDrive' -Name 'DisableFileSyncNGSC' -Type 'REG_DWORD' -Value '1'
& $Log "Disabling telemetry..."
Set-ISOScriptReg -Path 'HKLM\zNTUSER\Software\Microsoft\Windows\CurrentVersion\AdvertisingInfo' -Name 'Enabled' -Type 'REG_DWORD' -Value '0'
Set-ISOScriptReg -Path 'HKLM\zNTUSER\Software\Microsoft\Windows\CurrentVersion\Privacy' -Name 'TailoredExperiencesWithDiagnosticDataEnabled' -Type 'REG_DWORD' -Value '0'
Set-ISOScriptReg -Path 'HKLM\zNTUSER\Software\Microsoft\Speech_OneCore\Settings\OnlineSpeechPrivacy' -Name 'HasAccepted' -Type 'REG_DWORD' -Value '0'
Set-ISOScriptReg -Path 'HKLM\zNTUSER\Software\Microsoft\Input\TIPC' -Name 'Enabled' -Type 'REG_DWORD' -Value '0'
Set-ISOScriptReg -Path 'HKLM\zNTUSER\Software\Microsoft\InputPersonalization' -Name 'RestrictImplicitInkCollection' -Type 'REG_DWORD' -Value '1'
Set-ISOScriptReg -Path 'HKLM\zNTUSER\Software\Microsoft\InputPersonalization' -Name 'RestrictImplicitTextCollection' -Type 'REG_DWORD' -Value '1'
Set-ISOScriptReg -Path 'HKLM\zNTUSER\Software\Microsoft\InputPersonalization\TrainedDataStore' -Name 'HarvestContacts' -Type 'REG_DWORD' -Value '0'
Set-ISOScriptReg -Path 'HKLM\zNTUSER\Software\Microsoft\Personalization\Settings' -Name 'AcceptedPrivacyPolicy' -Type 'REG_DWORD' -Value '0'
Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Policies\Microsoft\Windows\DataCollection' -Name 'AllowTelemetry' -Type 'REG_DWORD' -Value '0'
Set-ISOScriptReg -Path 'HKLM\zSYSTEM\ControlSet001\Services\dmwappushservice' -Name 'Start' -Type 'REG_DWORD' -Value '4'
& $Log "Preventing installation of DevHome and Outlook..."
Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Orchestrator\UScheduler_Oobe\OutlookUpdate' -Name 'workCompleted' -Type 'REG_DWORD' -Value '1'
Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Orchestrator\UScheduler\OutlookUpdate' -Name 'workCompleted' -Type 'REG_DWORD' -Value '1'
Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Orchestrator\UScheduler\DevHomeUpdate' -Name 'workCompleted' -Type 'REG_DWORD' -Value '1'
Remove-ISOScriptReg 'HKLM\zSOFTWARE\Microsoft\WindowsUpdate\Orchestrator\UScheduler_Oobe\OutlookUpdate'
Remove-ISOScriptReg 'HKLM\zSOFTWARE\Microsoft\WindowsUpdate\Orchestrator\UScheduler_Oobe\DevHomeUpdate'
& $Log "Disabling Copilot..."
Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Policies\Microsoft\Windows\WindowsCopilot' -Name 'TurnOffWindowsCopilot' -Type 'REG_DWORD' -Value '1'
Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Policies\Microsoft\Edge' -Name 'HubsSidebarEnabled' -Type 'REG_DWORD' -Value '0'
Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Policies\Microsoft\Windows\Explorer' -Name 'DisableSearchBoxSuggestions' -Type 'REG_DWORD' -Value '1'
& $Log "Disabling Windows Update during OOBE (re-enabled on first logon via FirstLogon.ps1)..."
Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU' -Name 'NoAutoUpdate' -Type 'REG_DWORD' -Value '1'
Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU' -Name 'AUOptions' -Type 'REG_DWORD' -Value '1'
Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU' -Name 'UseWUServer' -Type 'REG_DWORD' -Value '1'
Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Policies\Microsoft\Windows\WindowsUpdate' -Name 'DisableWindowsUpdateAccess' -Type 'REG_DWORD' -Value '1'
Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Policies\Microsoft\Windows\WindowsUpdate' -Name 'WUServer' -Type 'REG_SZ' -Value 'http://localhost:8080'
Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Policies\Microsoft\Windows\WindowsUpdate' -Name 'WUStatusServer' -Type 'REG_SZ' -Value 'http://localhost:8080'
Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Orchestrator\UScheduler_Oobe\WindowsUpdate' -Name 'workCompleted' -Type 'REG_DWORD' -Value '1'
Remove-ISOScriptReg 'HKLM\zSOFTWARE\Microsoft\WindowsUpdate\Orchestrator\UScheduler_Oobe\WindowsUpdate'
Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Microsoft\Windows\CurrentVersion\DeliveryOptimization\Config' -Name 'DODownloadMode' -Type 'REG_DWORD' -Value '0'
Set-ISOScriptReg -Path 'HKLM\zSYSTEM\ControlSet001\Services\BITS' -Name 'Start' -Type 'REG_DWORD' -Value '4'
Set-ISOScriptReg -Path 'HKLM\zSYSTEM\ControlSet001\Services\wuauserv' -Name 'Start' -Type 'REG_DWORD' -Value '4'
Set-ISOScriptReg -Path 'HKLM\zSYSTEM\ControlSet001\Services\UsoSvc' -Name 'Start' -Type 'REG_DWORD' -Value '4'
Set-ISOScriptReg -Path 'HKLM\zSYSTEM\ControlSet001\Services\WaaSMedicSvc' -Name 'Start' -Type 'REG_DWORD' -Value '4'
& $Log "Preventing installation of Teams..."
Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Policies\Microsoft\Teams' -Name 'DisableInstallation' -Type 'REG_DWORD' -Value '1'
& $Log "Preventing installation of new Outlook..."
Set-ISOScriptReg -Path 'HKLM\zSOFTWARE\Policies\Microsoft\Windows\Windows Mail' -Name 'PreventRun' -Type 'REG_DWORD' -Value '1'
& $Log "Unloading offline registry hives..."
reg unload HKLM\zCOMPONENTS
reg unload HKLM\zDEFAULT
reg unload HKLM\zNTUSER
reg unload HKLM\zSOFTWARE
reg unload HKLM\zSYSTEM
# -- 4. Delete scheduled task definition files -----------------------------
& $Log "Deleting scheduled task definition files..."
$tasksPath = "$ScratchDir\Windows\System32\Tasks"
Remove-Item "$tasksPath\Microsoft\Windows\Application Experience\Microsoft Compatibility Appraiser" -Force
Remove-Item "$tasksPath\Microsoft\Windows\Customer Experience Improvement Program" -Recurse -Force
Remove-Item "$tasksPath\Microsoft\Windows\Application Experience\ProgramDataUpdater" -Force
Remove-Item "$tasksPath\Microsoft\Windows\Chkdsk\Proxy" -Force
Remove-Item "$tasksPath\Microsoft\Windows\Windows Error Reporting\QueueReporting" -Force
Remove-Item "$tasksPath\Microsoft\Windows\InstallService" -Recurse -Force
Remove-Item "$tasksPath\Microsoft\Windows\UpdateOrchestrator" -Recurse -Force
Remove-Item "$tasksPath\Microsoft\Windows\UpdateAssistant" -Recurse -Force
Remove-Item "$tasksPath\Microsoft\Windows\WaaSMedic" -Recurse -Force
Remove-Item "$tasksPath\Microsoft\Windows\WindowsUpdate" -Recurse -Force
Remove-Item "$tasksPath\Microsoft\WindowsUpdate" -Recurse -Force
& $Log "Scheduled task files deleted."
# -- 5. Remove ISO support folder -----------------------------------------
if ($ISOContentsDir -and (Test-Path $ISOContentsDir)) {
& $Log "Removing ISO support\ folder..."
Remove-Item -Path (Join-Path $ISOContentsDir "support") -Recurse -Force
& $Log "ISO support\ folder removed."
Add-WinUtilISOStagedDrivers -ContentRoot $ISOContentsDir -Logger $Log -InstallImagePath $InstallImagePath -InstallImageIndex $InstallImageIndex
}
}
+28 -10
View File
@@ -8,7 +8,7 @@ function Invoke-WinUtilISORefreshUSBDrives {
$combo.Items.Add("No USB drives detected.")
$combo.SelectedIndex = 0
$sync["Win11ISOUSBDisks"] = @()
Write-Win11ISOLog "No USB drives detected."
Write-WinUtilISOLog "No USB drives detected."
return
}
@@ -17,7 +17,7 @@ function Invoke-WinUtilISORefreshUSBDrives {
$combo.Items.Add("Disk $($disk.Number): $($disk.FriendlyName) [$sizeGB GB] - $($disk.PartitionStyle)")
}
$combo.SelectedIndex = 0
Write-Win11ISOLog "Found $($removable.Count) USB drive(s)."
Write-WinUtilISOLog "Found $($removable.Count) USB drive(s)."
$sync["Win11ISOUSBDisks"] = $removable
}
@@ -30,6 +30,20 @@ function Invoke-WinUtilISOWriteUSB {
return
}
$installWim = Join-Path $contentsDir "sources\install.wim"
$installEsd = Join-Path $contentsDir "sources\install.esd"
if (Test-Path $installEsd) {
$installEsdFile = Get-Item $installEsd
$esdSizeBytes = $installEsdFile.Length
$esdSizeMB = [math]::Ceiling($esdSizeBytes / 1MB)
if ($esdSizeBytes -ge 4GB) {
[System.Windows.MessageBox]::Show(
"This ISO uses an install.esd file that is $esdSizeMB MB. WinUtil's FAT32 USB format cannot store files larger than 4 GB.`n`nExport an ISO instead or use media with install.wim.",
"USB Creation Not Supported", "OK", "Warning")
return
}
}
$combo = $sync["WPFWin11ISOUSBDriveComboBox"]
$selectedIndex = $combo.SelectedIndex
$selectedItemText = [string]$combo.SelectedItem
@@ -56,12 +70,13 @@ function Invoke-WinUtilISOWriteUSB {
"Confirm USB Erase", "YesNo", "Warning")
if ($confirm -ne "Yes") {
Write-Win11ISOLog "USB write cancelled by user."
Write-WinUtilISOLog "USB write cancelled by user."
return
}
$sync["WPFWin11ISOWriteUSBButton"].IsEnabled = $false
Write-Win11ISOLog "Starting USB write to Disk $diskNum..."
$sync["Win11ISOProcessRunning"] = $true
Write-WinUtilISOLog "Starting USB write to Disk $diskNum..."
$runspace = [Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace()
$runspace.ApartmentState = "STA"
@@ -86,9 +101,10 @@ function Invoke-WinUtilISOWriteUSB {
function SetProgress($label, $pct) {
$sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{
$sync.progressBarTextBlock.Text = $label
$sync.progressBarTextBlock.ToolTip = $label
$sync.ProgressBar.Value = [Math]::Max($pct, 5)
$sync["WPFTweaksProgressBar"].Visibility = "Visible"
$sync["WPFTweaksProgressLabel"].Text = $label
$sync["WPFTweaksProgressLabel"].ToolTip = $label
$sync["WPFTweaksProgressValue"].Value = [Math]::Max($pct, 5)
})
}
@@ -255,10 +271,12 @@ function Invoke-WinUtilISOWriteUSB {
})
} finally {
Start-Sleep -Milliseconds 800
$sync["Win11ISOProcessRunning"] = $false
$sync["WPFWin11ISOStatusLog"].Dispatcher.Invoke([action]{
$sync.progressBarTextBlock.Text = ""
$sync.progressBarTextBlock.ToolTip = ""
$sync.ProgressBar.Value = 0
$sync["WPFTweaksProgressBar"].Visibility = "Collapsed"
$sync["WPFTweaksProgressLabel"].Text = ""
$sync["WPFTweaksProgressLabel"].ToolTip = ""
$sync["WPFTweaksProgressValue"].Value = 0
$sync["WPFWin11ISOWriteUSBButton"].IsEnabled = $true
})
}
@@ -78,6 +78,7 @@ function Invoke-WinUtilTweaks {
$sync.configs.tweaks.$CheckBox.appx | ForEach-Object {
Remove-WinUtilAPPX -Name $psitem
}
Remove-WinUtilProvisionedAPPX -PackageList $sync.configs.tweaks.$CheckBox.appx
}
}
Write-WinUtilLog -Component "Tweaks" -Message "$action tweak completed: $CheckBox"
+15 -2
View File
@@ -17,7 +17,20 @@ function Remove-WinUtilAPPX {
Write-Host "Removing $Name"
Write-WinUtilLog -Component "AppX" -Message "Removing AppX package pattern: $Name"
Get-AppxPackage $Name -AllUsers | Remove-AppxPackage -AllUsers
Get-AppxProvisionedPackage -Online | Where-Object DisplayName -like $Name | Remove-AppxProvisionedPackage -Online
# We explicitly loop through packages instead of using the pipeline because PowerShell 7 pipeline binding
# for Remove-AppxPackage fails silently, and Get-AppxPackage -AllUsers returns duplicate objects for each user profile.
$pkgs = Get-AppxPackage "*$Name*" -AllUsers | Sort-Object -Property PackageFullName -Unique
if ($null -ne $pkgs) {
foreach ($pkg in $pkgs) {
try {
Remove-AppxPackage -Package $pkg.PackageFullName -AllUsers -ErrorAction Stop
}
catch {
Write-WinUtilLog -Level "ERROR" -Component "AppX" -Message "Failed to remove AppX package $($pkg.PackageFullName): $($_.Exception.Message)"
}
}
}
Write-WinUtilLog -Component "AppX" -Message "AppX removal completed for package pattern: $Name"
}
@@ -0,0 +1,62 @@
function Remove-WinUtilProvisionedAPPX {
<#
.SYNOPSIS
Removes all AppX provisioned packages that match the given names
.PARAMETER PackageList
An array of names of the APPX packages to remove
.EXAMPLE
Remove-WinUtilProvisionedAPPX -PackageList @("Microsoft.Microsoft3DViewer", "Microsoft.WindowsCalculator")
#>
param (
[string[]]$PackageList
)
if ($null -eq $PackageList -or $PackageList.Count -eq 0) {
return
}
Write-Host "`nRemoving provisioned packages..."
Write-WinUtilLog -Component "AppX" -Message "Removing AppX provisioned packages: $($PackageList -join ', ')"
# DISM cmdlets like Get-AppxProvisionedPackage often fail with "Class not registered" or hang in PowerShell 7.
# We shell out to Windows PowerShell 5.1 (powershell.exe) to reliably remove the provisioned packages.
$ps5Command = {
$pkgs = $args
$provisionedPackages = Get-AppxProvisionedPackage -Online -ErrorAction SilentlyContinue
$failures = [System.Collections.Generic.List[string]]::new()
foreach ($Package in $pkgs) {
$provs = $provisionedPackages |
Where-Object DisplayName -Like "*$Package*"
if ($null -ne $provs) {
foreach ($prov in $provs) {
try {
Remove-AppxProvisionedPackage -Online -PackageName $prov.PackageName -ErrorAction Stop | Out-Null
}
catch {
$failures.Add("Failed to remove provisioned AppX package $($prov.PackageName): $($_.Exception.Message)")
}
}
}
}
if ($failures.Count -gt 0) {
throw ($failures -join [Environment]::NewLine)
}
}
$removalOutput = powershell.exe -NoProfile -NonInteractive -Command $ps5Command -args $PackageList 2>&1
if ($LASTEXITCODE -ne 0 -or $null -ne $removalOutput) {
$failureDetails = ($removalOutput | Out-String).Trim()
$errorMessage = "AppX provisioned package removal failed: $failureDetails"
Write-WinUtilLog -Level "ERROR" -Component "AppX" -Message $errorMessage
throw $errorMessage
}
Write-WinUtilLog -Component "AppX" -Message "AppX provisioned package removal completed."
}
+59
View File
@@ -0,0 +1,59 @@
function Save-WinUtilFile {
<#
.SYNOPSIS
Downloads a file and reports transfer progress.
#>
param(
[Parameter(Mandatory)]
[uri]$Uri,
[Parameter(Mandatory)]
[string]$DestinationPath,
[Parameter(Mandatory)]
[scriptblock]$ProgressCallback
)
$response = $null
$responseStream = $null
$outputStream = $null
try {
$request = [System.Net.WebRequest]::Create($Uri)
$response = $request.GetResponse()
$totalBytes = $response.ContentLength
$responseStream = $response.GetResponseStream()
$outputStream = [System.IO.File]::Create($DestinationPath)
$buffer = New-Object byte[] 81920
$downloadedBytes = 0L
$lastPercent = -1
while (($bytesRead = $responseStream.Read($buffer, 0, $buffer.Length)) -gt 0) {
$outputStream.Write($buffer, 0, $bytesRead)
$downloadedBytes += $bytesRead
if ($totalBytes -gt 0) {
$percent = [Math]::Min(100, [int](($downloadedBytes / $totalBytes) * 100))
if ($percent -ne $lastPercent) {
& $ProgressCallback $percent
$lastPercent = $percent
}
}
}
if ($lastPercent -ne 100) {
& $ProgressCallback 100
}
}
finally {
if ($null -ne $outputStream) {
$outputStream.Dispose()
}
if ($null -ne $responseStream) {
$responseStream.Dispose()
}
if ($null -ne $response) {
$response.Dispose()
}
}
}
@@ -0,0 +1,17 @@
function Set-WinUtilAppCategoryFilter {
<#
.SYNOPSIS
Applies an exact application category filter from an Install tab search chip.
.PARAMETER Category
The application category to show. An empty value clears the filter.
#>
param(
[Parameter(Mandatory = $false)]
[string]$Category = ""
)
$sync.SearchBar.Tag = $Category
$sync.SearchBar.Text = $Category
Find-AppsByNameOrDescription -SearchString $Category -Category $Category
}
+51 -3
View File
@@ -33,23 +33,71 @@ function Set-WinUtilDNS {
}
}
$dohSupported = [bool](Get-Command Add-DnsClientDohServerAddress -ErrorAction SilentlyContinue)
$dnscacheBase = "HKLM:\System\CurrentControlSet\Services\Dnscache\InterfaceSpecificParameters"
Foreach ($Adapter in $Adapters) {
$interfaceParams = "$dnscacheBase\$($Adapter.InterfaceGuid)"
if($DNSProvider -eq "DHCP") {
Write-WinUtilLog -Component "DNS" -Message "Resetting DNS to DHCP on adapter $($Adapter.Name) (ifIndex: $($Adapter.ifIndex))."
Set-DnsClientServerAddress -InterfaceIndex $Adapter.ifIndex -ResetServerAddresses
netsh interface ip set dnsservers name="$($Adapter.Name)" source=dhcp
netsh interface ipv6 set dnsservers name="$($Adapter.Name)" source=dhcp
$dohInterfaceSettings = "$interfaceParams\DohInterfaceSettings"
if (Test-Path $dohInterfaceSettings) {
if ($dohSupported) {
$dohServerAddresses = @(
Get-ChildItem -Path "$dohInterfaceSettings\Doh" -ErrorAction SilentlyContinue
Get-ChildItem -Path "$dohInterfaceSettings\Doh6" -ErrorAction SilentlyContinue
) | Select-Object -ExpandProperty PSChildName -Unique
foreach ($ip in $dohServerAddresses) {
if (Get-DnsClientDohServerAddress -ServerAddress $ip -ErrorAction SilentlyContinue) {
Write-WinUtilLog -Component "DNS" -Message "Removing DoH registration for $ip."
Remove-DnsClientDohServerAddress -ServerAddress $ip -Confirm:$false -ErrorAction Stop
}
}
}
Remove-Item -Path $dohInterfaceSettings -Recurse -Force -ErrorAction SilentlyContinue
}
} else {
Write-WinUtilLog -Component "DNS" -Message "Setting IPv4 DNS on adapter $($Adapter.Name) (ifIndex: $($Adapter.ifIndex)) to $($dns.Primary), $($dns.Secondary)."
Set-DnsClientServerAddress -InterfaceIndex $Adapter.ifIndex -ServerAddresses ($dns.Primary, $dns.Secondary)
Write-WinUtilLog -Component "DNS" -Message "Setting IPv6 DNS on adapter $($Adapter.Name) (ifIndex: $($Adapter.ifIndex)) to $($dns.Primary6), $($dns.Secondary6)."
Set-DnsClientServerAddress -InterfaceIndex $Adapter.ifIndex -ServerAddresses ($dns.Primary6, $dns.Secondary6)
if ($dohSupported -and $dns.DohTemplate) {
$ips = @($dns.Primary, $dns.Secondary, $dns.Primary6, $dns.Secondary6) | Where-Object { $_ }
foreach ($ip in $ips) {
$existing = Get-DnsClientDohServerAddress -ServerAddress $ip -ErrorAction SilentlyContinue
if ($existing) {
Set-DnsClientDohServerAddress -ServerAddress $ip -DohTemplate $dns.DohTemplate -AllowFallbackToUdp $false -AutoUpgrade $true -ErrorAction Stop
} else {
Write-WinUtilLog -Component "DNS" -Message "Registering DoH template for $ip."
Add-DnsClientDohServerAddress -ServerAddress $ip -DohTemplate $dns.DohTemplate -AllowFallbackToUdp $false -AutoUpgrade $true -ErrorAction Stop
}
$leaf = if ($ip.Contains(':')) { 'Doh6' } else { 'Doh' }
$regPath = "$interfaceParams\DohInterfaceSettings\$leaf\$ip"
if (-not (Test-Path $regPath)) {
New-Item -Path $regPath -Force -ErrorAction Stop | Out-Null
}
New-ItemProperty -Path $regPath -Name "DohFlags" -Value 1 -PropertyType QWord -Force -ErrorAction Stop | Out-Null
}
}
}
}
if ($DNSProvider -ne "DHCP" -and $dohSupported -and $dns.DohTemplate) {
Clear-DnsClientCache
}
Write-WinUtilLog -Component "DNS" -Message "DNS provider change completed: $DNSProvider"
} catch {
Write-Warning "Unable to set DNS Provider due to an unhandled exception."
Write-Warning $psitem.Exception.StackTrace
Write-WinUtilLog -Level "ERROR" -Component "DNS" -Message "Unable to set DNS provider $DNSProvider`: $($psitem.Exception.Message)"
Write-Warning "DNS provider $DNSProvider was not completed because an error occurred."
Write-Warning $psitem.Exception.Message
Write-WinUtilLog -Level "ERROR" -Component "DNS" -Message "DNS provider $DNSProvider was not completed: $($psitem.Exception.Message)"
}
}
@@ -1,26 +0,0 @@
function Set-WinUtilProgressbar{
<#
.SYNOPSIS
This function is used to Update the Progress Bar displayed in the winutil GUI.
It will be automatically hidden if the user clicks something and no process is running
.PARAMETER Label
The Text to be overlaid onto the Progress Bar
.PARAMETER PERCENT
The percentage of the Progress Bar that should be filled (0-100)
#>
param(
[string]$Label,
[ValidateRange(0,100)]
[int]$Percent
)
$progressLabel = $Label
Invoke-WPFUIThread -ScriptBlock {$sync.progressBarTextBlock.Text = $progressLabel}
Invoke-WPFUIThread -ScriptBlock {$sync.progressBarTextBlock.ToolTip = $progressLabel}
if ($Percent -lt 5 ) {
$Percent = 5 # Ensure the progress bar is not empty, as it looks weird
}
Invoke-WPFUIThread -ScriptBlock { $sync.ProgressBar.Value = $Percent}
}
@@ -0,0 +1,35 @@
function Set-WinUtilTweaksProgressIndicator {
<#
.SYNOPSIS
Shows, updates, or hides the window-level progress indicator used by long-running
workflows such as app management, Tweaks, AppX management, and Win11 Creator.
It lives outside the TabControl, so it stays visible no matter which tab is active.
.PARAMETER Visible
Whether the indicator should be shown or hidden.
.PARAMETER Label
The text to display above the progress bar.
.PARAMETER Percent
The percentage of the progress bar that should be filled (0-100).
#>
param(
[bool]$Visible,
[string]$Label,
[ValidateRange(0,100)]
[int]$Percent
)
$indicatorVisible = if ($Visible) { [Windows.Visibility]::Visible } else { [Windows.Visibility]::Collapsed }
$indicatorLabel = $Label
$hasLabel = $PSBoundParameters.ContainsKey('Label')
$hasPercent = $PSBoundParameters.ContainsKey('Percent')
Invoke-WPFUIThread -ScriptBlock {
$sync.WPFTweaksProgressBar.Visibility = $indicatorVisible
if ($hasLabel) {
$sync.WPFTweaksProgressLabel.Text = $indicatorLabel
}
if ($hasPercent) {
$sync.WPFTweaksProgressValue.Value = $Percent
}
}
}
@@ -1,23 +0,0 @@
function Show-WPFInstallAppBusy {
<#
.SYNOPSIS
Displays a busy overlay in the install app area of the WPF form.
This is used to indicate that an install or uninstall is in progress.
Dynamically updates the size of the overlay based on the app area on each invocation.
.PARAMETER text
The text to display in the busy overlay. Defaults to "Installing apps...".
#>
param (
$text = "Installing apps..."
)
$overlayText = $text
Invoke-WPFUIThread -ScriptBlock {
$sync.InstallAppAreaOverlay.Visibility = [Windows.Visibility]::Visible
$sync.InstallAppAreaOverlay.Width = $($sync.InstallAppAreaScrollViewer.ActualWidth * 0.4)
$sync.InstallAppAreaOverlay.Height = $($sync.InstallAppAreaScrollViewer.ActualWidth * 0.4)
$sync.InstallAppAreaOverlayText.Text = $overlayText
$sync.InstallAppAreaBorder.IsEnabled = $false
$sync.InstallAppAreaScrollViewer.Effect.Radius = 5
}
}
@@ -9,7 +9,7 @@ function Invoke-WinUtilInstallAppRenderBatch {
}
if ($sync.currentTab -eq "Install" -and $sync.SearchBar -and -not [string]::IsNullOrWhiteSpace($sync.SearchBar.Text)) {
Find-AppsByNameOrDescription -SearchString $sync.SearchBar.Text
Find-AppsByNameOrDescription -SearchString $sync.SearchBar.Text -Category $sync.SearchBar.Tag
}
}
@@ -0,0 +1,68 @@
function Invoke-WPFAppxInstall {
if ($sync.ProcessRunning) {
Show-WinUtilMessage -Message "An AppX process is currently running." -Title "WinUtil" -Button "OK" -Icon "Warning"
return
}
if ($null -eq $sync.selectedAppx -or $sync.selectedAppx.Count -eq 0) {
Show-WinUtilMessage -Message "No AppX Package selected" -Title "Error" -Button "OK" -Icon "Error"
return
}
$selected = @($sync.selectedAppx)
$apps = $sync.configs.appxHashtable
$sync.ProcessRunning = $true
Invoke-WPFRunspace -ParameterList @(("selected", $selected), ("apps", $apps)) -ScriptBlock {
param($selected, $apps)
$totalPackages = @($selected).Count
$hasUI = $null -ne $sync.Form -and $null -ne $sync.Form.Dispatcher
try {
Write-WinUtilLog -Component "AppX" -Message "Starting AppX install for $totalPackages selected package(s)."
if ($hasUI) {
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "Preparing AppX install (0/$totalPackages)" -Percent 0
Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -state "Normal" -value 0.01 -overlay "logo" }
}
for ($index = 0; $index -lt $totalPackages; $index++) {
$key = $selected[$index]
$app = $apps[$key]
$position = $index + 1
$startPercent = [int](($index / $totalPackages) * 100)
if ($hasUI) {
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "Installing $($app.Content) ($position/$totalPackages)" -Percent $startPercent
}
Write-Host "Installing $($app.Content)"
Install-WinUtilAPPX -Name $app.PackageId -StoreId $app.StoreId
$completedPercent = [int](($position / $totalPackages) * 100)
if ($hasUI) {
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "Installed $($app.Content) ($position/$totalPackages)" -Percent $completedPercent
Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -value ($completedPercent / 100) }
}
}
Write-Host "================================="
Write-Host "-- AppX Install Finished ---"
Write-Host "================================="
Write-WinUtilLog -Component "AppX" -Message "AppX install finished."
if ($hasUI) {
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "AppX install finished" -Percent 100
Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -state "None" -overlay "checkmark" }
}
}
catch {
Write-WinUtilLog -Level "ERROR" -Component "AppX" -Message "AppX install failed: $($_.Exception.Message)"
if ($hasUI) {
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "AppX install failed" -Percent 100
Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -state "Error" -overlay "warning" }
}
}
finally {
$sync.ProcessRunning = $false
}
}
}
+78 -29
View File
@@ -1,51 +1,100 @@
function Invoke-WPFAppxRemoval {
if ($sync.ProcessRunning) {
Show-WinUtilMessage -Message "An AppX process is currently running." -Title "WinUtil" -Button "OK" -Icon "Warning"
return
}
if ($null -eq $sync.selectedAppx -or $sync.selectedAppx.Count -eq 0) {
Show-WinUtilMessage -Message "No AppX Package selected" -Title "Error" -Button "OK" -Icon "Error"
return
}
$selected = $sync.selectedAppx
$selected = @($sync.selectedAppx)
$apps = $sync.configs.appxHashtable
$sync.ProcessRunning = $true
Invoke-WPFRunspace -ParameterList @(("selected", $selected), ("apps", $apps)) -ScriptBlock {
param($selected, $apps)
$sync.ProcessRunning = $true
Write-WinUtilLog -Component "AppX" -Message "Starting AppX removal for $(@($selected).Count) selected package(s)."
$totalPackages = @($selected).Count
$hasUI = $null -ne $sync.Form -and $null -ne $sync.Form.Dispatcher
$packageList = [System.Collections.Generic.List[string]]::new()
foreach ($key in $selected) {
if ($key -eq "WPFAppxMicrosoft_XboxGamingOverlay") {
# Making sure Game Bar isn't running
Write-WinUtilLog -Component "AppX" -Message "Stopping GameBarFTServer before removing Xbox Gaming Overlay."
Stop-Process -Name GameBarFTServer
# This stops annoying ms-gamebar popup when launching games.
Write-WinUtilLog -Component "AppX" -Message "Disabling Game DVR capture before removing Xbox Gaming Overlay."
Set-ItemProperty -Path HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\GameDVR -Name AppCaptureEnabled -Value 0
try {
Write-WinUtilLog -Component "AppX" -Message "Starting AppX removal for $totalPackages selected package(s)."
if ($hasUI) {
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "Preparing AppX removal (0/$totalPackages)" -Percent 0
Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -state "Normal" -value 0.01 -overlay "logo" }
}
if ($key -eq "WPFAppxMicrosoft_WindowsNotepad") {
# i hope your having fun reading this
Write-WinUtilLog -Component "AppX" -Message "Stopping dllhost before removing Notepad."
Stop-Process -Name dllhost
for ($index = 0; $index -lt $totalPackages; $index++) {
$key = $selected[$index]
$app = $apps[$key]
$position = $index + 1
$startPercent = [int](($index / $totalPackages) * 90)
if ($hasUI) {
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "Removing $($app.Content) ($position/$totalPackages)" -Percent $startPercent
}
if ($key -eq "WPFAppxMicrosoft_XboxGamingOverlay") {
# Making sure Game Bar isn't running
Write-WinUtilLog -Component "AppX" -Message "Stopping GameBarFTServer before removing Xbox Gaming Overlay."
Stop-Process -Name GameBarFTServer -Force -Confirm:$false -ErrorAction SilentlyContinue
# This stops annoying ms-gamebar popup when launching games.
Write-WinUtilLog -Component "AppX" -Message "Disabling Game DVR capture before removing Xbox Gaming Overlay."
Set-ItemProperty -Path HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\GameDVR -Name AppCaptureEnabled -Value 0
}
if ($key -eq "WPFAppxMicrosoft_WindowsNotepad") {
Write-WinUtilLog -Component "AppX" -Message "Stopping dllhost before removing Notepad."
Stop-Process -Name dllhost -Force -Confirm:$false -ErrorAction SilentlyContinue
}
Write-Host "Removing $($app.Content)"
Write-WinUtilLog -Component "AppX" -Message "Removing $($app.Content) ($($app.PackageId))."
Remove-WinUtilAPPX -Name $app.PackageId
$packageList.Add($app.PackageId)
if ($key -eq "WPFAppxMSTeams") {
# Uninstalls Microsoft Teams Meeting Add-in for Microsoft Office
Write-WinUtilLog -Component "AppX" -Message "Uninstalling Microsoft Teams meeting add-in package."
Get-Package -Name "Microsoft Teams*" -ErrorAction SilentlyContinue | Uninstall-Package -Force
}
$completedPercent = [int](($position / $totalPackages) * 90)
if ($hasUI) {
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "Removed $($app.Content) ($position/$totalPackages)" -Percent $completedPercent
Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -value ($completedPercent / 100) }
}
}
Write-Host "Removing $($apps[$key].Content)"
Write-WinUtilLog -Component "AppX" -Message "Removing $($apps[$key].Content) ($($apps[$key].PackageId))."
Get-AppxPackage -Name $apps[$key].PackageId -AllUsers | Remove-AppxPackage -AllUsers
if ($packageList.Count -gt 0) {
if ($hasUI) {
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "Removing provisioned AppX packages" -Percent 90
}
Remove-WinUtilProvisionedAPPX -PackageList $packageList.ToArray()
}
if ($key -eq "WPFAppxMSTeams") {
# Uninstalls Microsoft Teams Meeting Add-in for Microsoft Office
Write-WinUtilLog -Component "AppX" -Message "Uninstalling Microsoft Teams meeting add-in package."
Get-Package -Name "Microsoft Teams*" -ErrorAction SilentlyContinue | Uninstall-Package -Force
Write-Host "================================="
Write-Host "-- AppX Removal Finished ---"
Write-Host "================================="
Write-WinUtilLog -Component "AppX" -Message "AppX removal finished."
if ($hasUI) {
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "AppX removal finished" -Percent 100
Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -state "None" -overlay "checkmark" }
}
}
catch {
Write-WinUtilLog -Level "ERROR" -Component "AppX" -Message "AppX removal failed: $($_.Exception.Message)"
if ($hasUI) {
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "AppX removal failed" -Percent 100
Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -state "Error" -overlay "warning" }
}
}
finally {
$sync.ProcessRunning = $false
}
Write-Host "================================="
Write-Host "-- AppX Removal Finished ---"
Write-Host "================================="
Write-WinUtilLog -Component "AppX" -Message "AppX removal finished."
$sync.ProcessRunning = $false
}
}
+13 -3
View File
@@ -14,8 +14,8 @@ function Invoke-WPFButton {
# Use this to get the name of the button
#[System.Windows.MessageBox]::Show("$Button","Chris Titus Tech's Windows Utility","OK","Info")
if (-not $sync.ProcessRunning) {
Set-WinUtilProgressBar -label "" -percent 0
if (-not $sync.ProcessRunning -and -not $sync.Win11ISOProcessRunning) {
Set-WinUtilTweaksProgressIndicator -Visible $false
}
# Check if button is defined in feature config with function or InvokeScript
@@ -65,6 +65,9 @@ function Invoke-WPFButton {
"WPFUpdatessecurity" {Invoke-WPFUpdatessecurity}
"WPFGetInstalled" {Invoke-WPFGetInstalled -CheckBox "winget"}
"WPFGetInstalledTweaks" {Invoke-WPFGetInstalled -CheckBox "tweaks"}
"WPFAppxRemoval" {Invoke-WPFTab "WPFTab6BT"}
"WPFBackToTweaks" {Invoke-WPFTab "WPFTab2BT"}
"WPFInstallSelectedAppx" {Invoke-WPFAppxInstall}
"WPFRemoveSelectedAppx" {Invoke-WPFAppxRemoval}
"WPFDefaultAppxSelection" {Invoke-WPFPresets "AppxDefault" -checkboxfilterpattern "WPFAppx*"}
"WPFSelectAllAppx" {
@@ -74,7 +77,7 @@ function Invoke-WPFButton {
$sync.configs.appxHashtable.Keys | ForEach-Object {$sync.$_.IsChecked = $false}
}
"WPFGetInstalledAppx" {
$installedAppxPackages = Get-AppxPackage -AllUsers | Select-Object -ExpandProperty Name
$installedAppxPackages = Get-WinUtilInstalledAPPX
foreach ($appx in $sync.configs.appxHashtable.GetEnumerator()) {
if ($appx.Value.PackageId -in $installedAppxPackages) {
$sync.$($appx.Key).IsChecked = $true
@@ -83,6 +86,13 @@ function Invoke-WPFButton {
}
"WPFCloseButton" {$sync.Form.Close(); Write-Host "Bye bye!"}
"WPFMinimizeButton" {$sync.Form.WindowState = [Windows.WindowState]::Minimized}
"WPFMaximizeButton" {
if ($sync.Form.WindowState -eq [Windows.WindowState]::Normal) {
$sync.Form.WindowState = [Windows.WindowState]::Maximized
} else {
$sync.Form.WindowState = [Windows.WindowState]::Normal
}
}
"WPFselectedAppsButton" {$sync.selectedAppsPopup.IsOpen = -not $sync.selectedAppsPopup.IsOpen}
}
}
+63 -27
View File
@@ -1,6 +1,5 @@
function Invoke-WPFGetInstalled {
<#
TODO: Add the Option to use Chocolatey as Engine
.SYNOPSIS
Invokes the function that gets the checkboxes to check in a new runspace
@@ -19,35 +18,72 @@ function Invoke-WPFGetInstalled {
return
}
$managerPreference = $sync.preferences.packagemanager
Invoke-WPFRunspace -ParameterList @(("managerPreference", $managerPreference),("checkbox", $checkbox)) -ScriptBlock {
param (
[string]$checkbox,
[string]$managerPreference
$operation = [Hashtable]::Synchronized(@{
Checkboxes = @()
Error = $null
})
$completeAction = [Action[hashtable, string]]{
param(
[hashtable]$completedOperation,
[string]$completedCheckbox
)
$sync.ProcessRunning = $true
Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -state "Indeterminate" }
try {
if ($completedOperation.Error) {
Write-WinUtilLog -Level "ERROR" -Component "Install" -Message "Get installed state failed: $($completedOperation.Error)"
Write-Warning "Unable to get installed state: $($completedOperation.Error)"
return
}
if ($checkbox -eq "winget") {
Write-Host "Getting Installed Programs..."
switch ($managerPreference) {
"Choco"{$Checkboxes = Invoke-WinUtilCurrentSystem -CheckBox "choco"; break}
"Winget"{$Checkboxes = Invoke-WinUtilCurrentSystem -CheckBox $checkbox; break}
if ($completedCheckbox -eq "winget") {
foreach ($checkboxName in $completedOperation.Checkboxes) {
if (-not $sync.selectedApps.Contains($checkboxName)) {
$sync.selectedApps.Add($checkboxName)
}
}
Reset-WPFCheckBoxes -checkboxfilterpattern "WPFInstall*"
} else {
foreach ($checkboxName in $completedOperation.Checkboxes) {
$sync.$checkboxName.ischecked = $True
}
}
} finally {
$sync.ProcessRunning = $false
Set-WinUtilTaskbaritem -state "None"
}
}
$sync.ProcessRunning = $true
Set-WinUtilTaskbaritem -state "Indeterminate"
try {
Invoke-WPFRunspace -ParameterList @(
("managerPreference", $managerPreference),
("checkbox", $checkbox),
("operation", $operation),
("completeAction", $completeAction)
) -ScriptBlock {
param (
[string]$checkbox,
[string]$managerPreference,
[hashtable]$operation,
[Action[hashtable, string]]$completeAction
)
try {
if ($checkbox -eq "winget") {
switch ($managerPreference) {
"Choco" { $operation.Checkboxes = @(Invoke-WinUtilCurrentSystem -CheckBox "choco"); break }
"Winget" { $operation.Checkboxes = @(Invoke-WinUtilCurrentSystem -CheckBox $checkbox); break }
}
} elseif ($checkbox -eq "tweaks") {
$operation.Checkboxes = @(Invoke-WinUtilCurrentSystem -CheckBox $checkbox)
}
} catch {
$operation.Error = $_.Exception.Message
} finally {
$sync.Form.Dispatcher.BeginInvoke($completeAction, [object[]]@($operation, $checkbox)) | Out-Null
}
}
elseif ($checkbox -eq "tweaks") {
Write-Host "Getting Installed Tweaks..."
$Checkboxes = Invoke-WinUtilCurrentSystem -CheckBox $checkbox
}
$sync.form.Dispatcher.invoke({
foreach ($checkbox in $Checkboxes) {
$sync.$checkbox.ischecked = $True
}
})
Write-Host "Done..."
$sync.ProcessRunning = $false
Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -state "None" }
} catch {
$operation.Error = $_.Exception.Message
$completeAction.Invoke($operation, $checkbox)
}
}
+54 -6
View File
@@ -31,33 +31,81 @@ function Invoke-WPFInstall {
$packagesWinget = $packagesSorted['Winget']
$packagesChoco = $packagesSorted['Choco']
$totalPackages = @($packagesWinget).Count + @($packagesChoco).Count
$completedPackages = 0
$hasUI = $null -ne $sync.Form -and $null -ne $sync.Form.Dispatcher
Write-WinUtilLog -Component "Install" -Message "Install package manager split: winget=$(@($packagesWinget).Count), choco=$(@($packagesChoco).Count)"
try {
$sync.ProcessRunning = $true
if ($hasUI) {
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "Preparing app install (0/$totalPackages)" -Percent 0
Invoke-WPFUIThread -ScriptBlock {
if ($null -ne $sync.ItemsControl) {
$sync.ItemsControl.IsEnabled = $false
}
}
}
if($packagesWinget.Count -gt 0 -and $packagesWinget -ne "0") {
Show-WPFInstallAppBusy -text "Installing apps..."
Install-WinUtilWinget
Install-WinUtilProgramWinget -Action Install -Programs $packagesWinget
foreach ($program in $packagesWinget) {
$position = $completedPackages + 1
$startPercent = [int](($completedPackages / $totalPackages) * 100)
if ($hasUI) {
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "Installing $program ($position/$totalPackages)" -Percent $startPercent
}
Install-WinUtilProgramWinget -Action Install -Programs @($program)
$completedPackages++
$completedPercent = [int](($completedPackages / $totalPackages) * 100)
if ($hasUI) {
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "Installed $program ($completedPackages/$totalPackages)" -Percent $completedPercent
Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -value ($completedPercent / 100) }
}
}
}
if($packagesChoco.Count -gt 0) {
$position = $completedPackages + 1
$startPercent = [int](($completedPackages / $totalPackages) * 100)
if ($hasUI) {
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "Installing Chocolatey packages ($position/$totalPackages)" -Percent $startPercent
}
Install-WinUtilChoco
Install-WinUtilProgramChoco -Action Install -Programs $packagesChoco
$completedPackages += @($packagesChoco).Count
$completedPercent = [int](($completedPackages / $totalPackages) * 100)
if ($hasUI) {
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "Installed Chocolatey packages ($completedPackages/$totalPackages)" -Percent $completedPercent
Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -value ($completedPercent / 100) }
}
}
Hide-WPFInstallAppBusy
Write-Host "==========================================="
Write-Host "-- Installs have finished ---"
Write-Host "==========================================="
Write-WinUtilLog -Component "Install" -Message "Install workflow completed."
Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -state "None" -overlay "checkmark" }
if ($hasUI) {
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "App install finished" -Percent 100
Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -state "None" -overlay "checkmark" }
}
} catch {
Hide-WPFInstallAppBusy
Write-Host "==========================================="
Write-Host "Error: $_"
Write-Host "==========================================="
Write-WinUtilLog -Level "ERROR" -Component "Install" -Message "Install workflow failed: $($_.Exception.Message)"
Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -state "Error" -overlay "warning" }
if ($hasUI) {
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "App install failed" -Percent 100
Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -state "Error" -overlay "warning" }
}
} finally {
if ($hasUI) {
Invoke-WPFUIThread -ScriptBlock {
if ($null -ne $sync.ItemsControl) {
$sync.ItemsControl.IsEnabled = $true
}
}
}
$sync.ProcessRunning = $False
}
}
+45 -7
View File
@@ -1,12 +1,50 @@
function Invoke-WPFOOSU {
try {
$ProgressPreference = 'SilentlyContinue'
if ($sync.ProcessRunning) {
Show-WinUtilMessage -Message "Another process is currently running." -Title "WinUtil" -Button "OK" -Icon "Warning"
return
}
Invoke-WebRequest -Uri https://dl5.oo-software.com/files/ooshutup10/OOSU10.exe -OutFile "$winutildir\ooshutup10.exe"
Start-Process -FilePath "$winutildir\ooshutup10.exe"
$downloadPath = Join-Path $sync.winutildir "ooshutup10.exe"
$sync.ProcessRunning = $true
$ProgressPreference = 'Continue'
} catch {
Write-Error "Couldn't download O&O ShutUp10. Please make sure you have an active Internet connection."
Invoke-WPFRunspace -ParameterList @(,("downloadPath", $downloadPath)) -ScriptBlock {
param($downloadPath)
$hasUI = $null -ne $sync.Form -and $null -ne $sync.Form.Dispatcher
try {
Write-WinUtilLog -Component "OOSU" -Message "Downloading O&O ShutUp10++."
if ($hasUI) {
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "Downloading O&O ShutUp10++ (0%)" -Percent 0
}
Save-WinUtilFile -Uri "https://dl5.oo-software.com/files/ooshutup10/OOSU10.exe" -DestinationPath $downloadPath -ProgressCallback {
param($percent)
if ($hasUI) {
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "Downloading O&O ShutUp10++ ($percent%)" -Percent $percent
}
}
if ($hasUI) {
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "Launching O&O ShutUp10++" -Percent 100
}
Start-Process -FilePath $downloadPath
Write-WinUtilLog -Component "OOSU" -Message "O&O ShutUp10++ launched."
if ($hasUI) {
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "O&O ShutUp10++ launched" -Percent 100
}
}
catch {
Write-WinUtilLog -Level "ERROR" -Component "OOSU" -Message "O&O ShutUp10++ download failed: $($_.Exception.Message)"
if ($hasUI) {
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "O&O ShutUp10++ download failed" -Percent 100
}
Write-Error "Couldn't download O&O ShutUp10. Please make sure you have an active Internet connection."
}
finally {
$sync.ProcessRunning = $false
}
}
}
+3 -2
View File
@@ -44,6 +44,8 @@ public sealed class WinUtilRunspaceCleanupState
public static class WinUtilRunspaceCleanup
{
public static readonly System.Threading.WaitOrTimerCallback Callback = Cleanup;
public static void Cleanup(object state, bool timedOut)
{
var cleanupState = state as WinUtilRunspaceCleanupState;
@@ -89,8 +91,7 @@ public static class WinUtilRunspaceCleanup
$cleanupState = [WinUtilRunspaceCleanupState]::new()
$cleanupState.PowerShell = $powershell
$cleanupState.Handle = $handle
$cleanupCallback = [System.Threading.WaitOrTimerCallback][WinUtilRunspaceCleanup]::Cleanup
[System.Threading.ThreadPool]::RegisterWaitForSingleObject($handle.AsyncWaitHandle, $cleanupCallback, $cleanupState, -1, $true) | Out-Null
[System.Threading.ThreadPool]::RegisterWaitForSingleObject($handle.AsyncWaitHandle, [WinUtilRunspaceCleanup]::Callback, $cleanupState, -1, $true) | Out-Null
# Return the handle
return $handle
@@ -7,11 +7,21 @@ function Invoke-WPFSelectedCheckboxesUpdate ($type, $checkboxName) {
'^WPFAppx' { 'selectedAppx' }
}
$selectionChanged = $false
if ($type -eq "Add") {
if (-not $sync.$listName.Contains($checkboxName)) {
$sync.$listName.Add($checkboxName)
$selectionChanged = $true
}
} else {
$sync.$listName.Remove($checkboxName)
$selectionChanged = $sync.$listName.Remove($checkboxName)
}
if ($listName -eq "selectedApps" -and $selectionChanged) {
$sync.WPFselectedAppsButton.Content = "Selected Apps: $($sync.selectedApps.Count)"
$sync.selectedAppsstackPanel.Children.Clear()
$sync.selectedApps | Sort-Object | ForEach-Object {
Add-SelectedAppsMenuItem -name $sync.configs.applicationsHashtable.$_.Content -key $_
}
}
}
+1 -2
View File
@@ -19,13 +19,12 @@ function Invoke-WPFTab {
$tabNumber = [int]($ClickedTab -replace "WPFTab","" -replace "BT","") - 1
$filter = Get-WinUtilVariables -Type ToggleButton | Where-Object {$psitem -like "WPFTab?BT"}
$sync.$tabNav.Items[$tabNumber].IsSelected = $true
($sync.GetEnumerator()).where{$psitem.Key -in $filter} | ForEach-Object {
if ($ClickedTab -ne $PSItem.name) {
$sync[$PSItem.Name].IsChecked = $false
} else {
$sync["$ClickedTab"].IsChecked = $true
$tabNumber = [int]($ClickedTab-replace "WPFTab","" -replace "BT","") - 1
$sync.$tabNav.Items[$tabNumber].IsSelected = $true
}
}
$sync.currentTab = $sync.$tabNav.Items[$tabNumber].Header
+7 -2
View File
@@ -404,11 +404,16 @@ function Invoke-WPFUIElements {
$textBlock.ToolTip = $entryInfo.Link
$textBlock.Style = $HoverTextBlockStyle
$textBlock.UseLayoutRounding = $true
$textBlock.VerticalAlignment = "Center"
$textBlock.SetResourceReference([Windows.Controls.Control]::FontSizeProperty, "FontSize")
$textBlock.Tag = $checkBox
$textBlock.Add_MouseUp({
[System.Object]$Sender = $args[0]
Start-Process $Sender.ToolTip -ErrorAction Stop
})
$updateLinkMargin = {
[System.Object]$Sender = $args[0]
$linkedCheckBox = $Sender.Tag
@@ -441,7 +446,7 @@ function Invoke-WPFUIElements {
$sync[$entryInfo.Name].Add_Unchecked({
[System.Object]$Sender = $args[0]
Invoke-WPFSelectedCheckboxesUpdate -type "Remove" -checkbox $Sender.name
Invoke-WPFSelectedCheckboxesUpdate -type "Remove" -checkboxName $Sender.name
})
}
}
+53 -6
View File
@@ -42,11 +42,21 @@ function Invoke-WPFUnInstall {
$packagesWinget = $packagesSorted['Winget']
$packagesChoco = $packagesSorted['Choco']
$totalPackages = @($packagesWinget).Count + @($packagesChoco).Count
$completedPackages = 0
$hasUI = $null -ne $sync.Form -and $null -ne $sync.Form.Dispatcher
Write-WinUtilLog -Component "Uninstall" -Message "Uninstall package manager split: winget=$(@($packagesWinget).Count), choco=$(@($packagesChoco).Count)"
try {
$sync.ProcessRunning = $true
Show-WPFInstallAppBusy -text "Uninstalling apps..."
if ($hasUI) {
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "Preparing app uninstall (0/$totalPackages)" -Percent 0
Invoke-WPFUIThread -ScriptBlock {
if ($null -ne $sync.ItemsControl) {
$sync.ItemsControl.IsEnabled = $false
}
}
}
if ($packagesWinget -contains "Microsoft.Edge") {
New-Item -Path "$Env:SystemRoot\SystemApps\Microsoft.MicrosoftEdge_8wekyb3d8bbwe\MicrosoftEdge.exe" -Force
@@ -54,25 +64,62 @@ function Invoke-WPFUnInstall {
# Uninstall all selected programs in new window
if($packagesWinget.Count -gt 0) {
Install-WinUtilProgramWinget -Action Uninstall -Programs $packagesWinget
foreach ($program in $packagesWinget) {
$position = $completedPackages + 1
$startPercent = [int](($completedPackages / $totalPackages) * 100)
if ($hasUI) {
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "Uninstalling $program ($position/$totalPackages)" -Percent $startPercent
}
Install-WinUtilProgramWinget -Action Uninstall -Programs @($program)
$completedPackages++
$completedPercent = [int](($completedPackages / $totalPackages) * 100)
if ($hasUI) {
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "Uninstalled $program ($completedPackages/$totalPackages)" -Percent $completedPercent
Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -value ($completedPercent / 100) }
}
}
}
if($packagesChoco.Count -gt 0) {
$position = $completedPackages + 1
$startPercent = [int](($completedPackages / $totalPackages) * 100)
if ($hasUI) {
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "Uninstalling Chocolatey packages ($position/$totalPackages)" -Percent $startPercent
}
Install-WinUtilProgramChoco -Action Uninstall -Programs $packagesChoco
$completedPackages += @($packagesChoco).Count
$completedPercent = [int](($completedPackages / $totalPackages) * 100)
if ($hasUI) {
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "Uninstalled Chocolatey packages ($completedPackages/$totalPackages)" -Percent $completedPercent
Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -value ($completedPercent / 100) }
}
}
Hide-WPFInstallAppBusy
Write-Host "==========================================="
Write-Host "-- Uninstalls have finished ---"
Write-Host "==========================================="
Write-WinUtilLog -Component "Uninstall" -Message "Uninstall workflow completed."
Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -state "None" -overlay "checkmark" }
if ($hasUI) {
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "App uninstall finished" -Percent 100
Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -state "None" -overlay "checkmark" }
}
} catch {
Hide-WPFInstallAppBusy
Write-Host "==========================================="
Write-Host "Error: $_"
Write-Host "==========================================="
Write-WinUtilLog -Level "ERROR" -Component "Uninstall" -Message "Uninstall workflow failed: $($_.Exception.Message)"
Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -state "Error" -overlay "warning" }
if ($hasUI) {
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "App uninstall failed" -Percent 100
Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -state "Error" -overlay "warning" }
}
} finally {
if ($hasUI) {
Invoke-WPFUIThread -ScriptBlock {
if ($null -ne $sync.ItemsControl) {
$sync.ItemsControl.IsEnabled = $true
}
}
}
$sync.ProcessRunning = $False
}
+43 -22
View File
@@ -5,22 +5,51 @@ function Invoke-WPFUpdatesdefault {
Resets Windows Update settings to default
#>
$ErrorActionPreference = 'SilentlyContinue'
Write-WinUtilLog -Component "Updates" -Message "Resetting Windows Update settings to default."
Write-Host "Removing Windows Update policy settings..." -ForegroundColor Green
Write-WinUtilLog -Component "Updates" -Message "Removing Windows Update policy registry paths."
Write-Host "Removing Windows Update settings managed by WinUtil..." -ForegroundColor Green
Write-WinUtilLog -Component "Updates" -Message "Removing Windows Update registry values managed by WinUtil."
Remove-Item -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" -Recurse -Force
Remove-Item -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\DeliveryOptimization" -Recurse -Force
Remove-Item -Path "HKLM:\SOFTWARE\Microsoft\WindowsUpdate\UX\Settings" -Recurse -Force
Remove-Item -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\Device Metadata" -Recurse -Force
Remove-Item -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\DriverSearching" -Recurse -Force
Remove-Item -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" -Recurse -Force
$registryValues = @(
@{
Path = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU"
Names = @("NoAutoUpdate", "AUOptions", "NoAutoRebootWithLoggedOnUsers", "AUPowerManagement")
},
@{
Path = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate"
Names = @("ExcludeWUDriversInQualityUpdate", "DeferFeatureUpdates", "DeferFeatureUpdatesPeriodInDays", "DeferQualityUpdates", "DeferQualityUpdatesPeriodInDays")
},
@{
Path = "HKLM:\SOFTWARE\Microsoft\WindowsUpdate\UX\Settings"
Names = @("BranchReadinessLevel", "DeferFeatureUpdatesPeriodInDays", "DeferQualityUpdatesPeriodInDays")
},
@{
Path = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\Device Metadata"
Names = @("PreventDeviceMetadataFromNetwork")
},
@{
Path = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\DriverSearching"
Names = @("DontPromptForWindowsUpdate", "DontSearchWindowsUpdate", "DriverUpdateWizardWuSearchEnabled")
},
@{
Path = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\DeliveryOptimization\Config"
Names = @("DODownloadMode")
}
)
Write-Host "Showing Windows Updates in settings..."
Write-WinUtilLog -Component "Updates" -Message "Showing Windows Update settings page."
Remove-ItemProperty -Path HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer -Name SettingsPageVisibility
foreach ($registryEntry in $registryValues) {
foreach ($valueName in $registryEntry.Names) {
Remove-ItemProperty -Path $registryEntry.Path -Name $valueName -ErrorAction SilentlyContinue
}
}
$explorerPolicyPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer"
$settingsPageVisibility = (Get-ItemProperty -Path $explorerPolicyPath -Name "SettingsPageVisibility" -ErrorAction SilentlyContinue).SettingsPageVisibility
if ($settingsPageVisibility -eq "hide:windowsupdate") {
Write-Host "Removing WinUtil's legacy Windows Update page restriction..."
Write-WinUtilLog -Component "Updates" -Message "Removing the legacy Windows Update settings page restriction."
Remove-ItemProperty -Path $explorerPolicyPath -Name "SettingsPageVisibility" -ErrorAction SilentlyContinue
}
Write-Host "Reenabling Windows Update Services..." -ForegroundColor Green
Write-WinUtilLog -Component "Updates" -Message "Restoring Windows Update service startup types."
@@ -35,12 +64,8 @@ function Invoke-WPFUpdatesdefault {
Write-Host "Restored UsoSvc to Automatic."
Write-WinUtilLog -Component "Updates" -Message "Starting UsoSvc service and restoring startup type to Automatic."
Start-Service -Name UsoSvc
Set-Service -Name UsoSvc -StartupType Automatic
Write-Host "Restored WaaSMedicSvc to Manual."
Write-WinUtilLog -Component "Updates" -Message "Restoring WaaSMedicSvc service to Manual."
Set-Service -Name WaaSMedicSvc -StartupType Manual
Start-Service -Name UsoSvc
Write-Host "Enabling update related scheduled tasks..." -ForegroundColor Green
Write-WinUtilLog -Component "Updates" -Message "Enabling update related scheduled tasks."
@@ -54,13 +79,9 @@ function Invoke-WPFUpdatesdefault {
'\Microsoft\WindowsUpdate\*'
foreach ($Task in $Tasks) {
Get-ScheduledTask -TaskPath $Task | Enable-ScheduledTask -ErrorAction SilentlyContinue
Get-ScheduledTask -TaskPath $Task -ErrorAction SilentlyContinue | Enable-ScheduledTask -ErrorAction SilentlyContinue
}
Write-Host "Windows Local Policies Reset to Default."
Write-WinUtilLog -Component "Updates" -Message "Resetting local security policy to defaults with secedit."
secedit /configure /cfg "$Env:SystemRoot\inf\defltbase.inf" /db defltbase.sdb
Write-Host "===================================================" -ForegroundColor Green
Write-Host "--- Windows Update Settings Reset to Default ---" -ForegroundColor Green
Write-Host "===================================================" -ForegroundColor Green
+20 -20
View File
@@ -8,7 +8,17 @@ function Invoke-WPFUpdatesdisable {
Disabling Windows Update is not recommended. This is only for advanced users who know what they are doing.
#>
$ErrorActionPreference = 'SilentlyContinue'
$confirmation = Show-WinUtilMessage `
-Message "Disabling Windows Update stops update services, disables scheduled tasks, and clears downloaded update files. Security updates will not be installed until defaults are restored. Continue?" `
-Title "Disable Windows Update?" `
-Button "YesNo" `
-Icon "Warning"
if ($confirmation -ne "Yes") {
Write-WinUtilLog -Component "Updates" -Message "Windows Update disable workflow cancelled."
return
}
Write-WinUtilLog -Component "Updates" -Message "Disabling Windows Update settings."
Write-Host "Configuring registry settings..." -ForegroundColor Yellow
@@ -21,24 +31,14 @@ function Invoke-WPFUpdatesdisable {
New-Item -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\DeliveryOptimization\Config" -Force
Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\DeliveryOptimization\Config" -Name "DODownloadMode" -Type DWord -Value 0
Write-Host "Hiding Windows Updates from settings..."
Write-WinUtilLog -Component "Updates" -Message "Hiding Windows Update settings page."
Set-ItemProperty -Path HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer -Name SettingsPageVisibility -Value hide:windowsupdate
foreach ($serviceName in @("BITS", "wuauserv", "UsoSvc")) {
Write-Host "Stopping and disabling $serviceName service."
Write-WinUtilLog -Component "Updates" -Message "Stopping and disabling $serviceName service."
Stop-Service -Name $serviceName -Force -ErrorAction SilentlyContinue
Set-Service -Name $serviceName -StartupType Disabled
}
Write-Host "Disabled BITS Service."
Write-WinUtilLog -Component "Updates" -Message "Disabling BITS service."
Set-Service -Name BITS -StartupType Disabled
Write-Host "Disabled wuauserv Service."
Write-WinUtilLog -Component "Updates" -Message "Disabling wuauserv service."
Set-Service -Name wuauserv -StartupType Disabled
Write-Host "Disabled UsoSvc Service."
Write-WinUtilLog -Component "Updates" -Message "Stopping and disabling UsoSvc service."
Stop-Service -Name UsoSvc -Force
Set-Service -Name UsoSvc -StartupType Disabled
Remove-Item "C:\Windows\SoftwareDistribution\*" -Recurse -Force
Remove-Item -Path "C:\Windows\SoftwareDistribution\*" -Recurse -Force -ErrorAction SilentlyContinue
Write-Host "Cleared SoftwareDistribution folder."
Write-WinUtilLog -Component "Updates" -Message "Cleared SoftwareDistribution folder."
@@ -54,11 +54,11 @@ function Invoke-WPFUpdatesdisable {
'\Microsoft\WindowsUpdate\*'
foreach ($Task in $Tasks) {
Get-ScheduledTask -TaskPath $Task | Disable-ScheduledTask -ErrorAction SilentlyContinue
Get-ScheduledTask -TaskPath $Task -ErrorAction SilentlyContinue | Disable-ScheduledTask -ErrorAction SilentlyContinue
}
Write-Host "=================================" -ForegroundColor Green
Write-Host "--- Updates Are Disabled ---" -ForegroundColor Green
Write-Host "--- Windows Update Is Disabled ---" -ForegroundColor Green
Write-Host "=================================" -ForegroundColor Green
Write-Host "Note: You must restart your system in order for all changes to take effect." -ForegroundColor Yellow
+47 -16
View File
@@ -6,10 +6,9 @@ function Invoke-WPFUpdatessecurity {
.DESCRIPTION
1. Disables driver offering through Windows Update
2. Disables Windows Update automatic restart
3. Sets Windows Update to Semi-Annual Channel (Targeted)
4. Defers feature updates for 365 days
5. Defers quality updates for 4 days
2. Defers feature updates for 365 days
3. Defers quality updates for 4 days
4. Prevents automatic restarts while a user is signed in
#>
@@ -17,6 +16,32 @@ function Invoke-WPFUpdatessecurity {
Write-WinUtilLog -Component "Updates" -Message "Applying recommended Windows Update settings."
Write-WinUtilLog -Component "Updates" -Message "Disabling driver offering through Windows Update."
$windowsUpdatePolicyPath = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate"
$automaticUpdatePolicyPath = Join-Path $windowsUpdatePolicyPath "AU"
Write-Host "Restoring Windows Update availability..."
Write-WinUtilLog -Component "Updates" -Message "Restoring Windows Update services and scheduled tasks before applying recommended settings."
Remove-ItemProperty -Path $automaticUpdatePolicyPath -Name "NoAutoUpdate" -ErrorAction SilentlyContinue
Remove-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\DeliveryOptimization\Config" -Name "DODownloadMode" -ErrorAction SilentlyContinue
Set-Service -Name BITS -StartupType Manual
Set-Service -Name wuauserv -StartupType Manual
Set-Service -Name UsoSvc -StartupType Automatic
Start-Service -Name UsoSvc
$Tasks =
'\Microsoft\Windows\InstallService\*',
'\Microsoft\Windows\UpdateOrchestrator\*',
'\Microsoft\Windows\UpdateAssistant\*',
'\Microsoft\Windows\WaaSMedic\*',
'\Microsoft\Windows\WindowsUpdate\*',
'\Microsoft\WindowsUpdate\*'
foreach ($Task in $Tasks) {
Get-ScheduledTask -TaskPath $Task -ErrorAction SilentlyContinue | Enable-ScheduledTask -ErrorAction SilentlyContinue
}
New-Item -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\Device Metadata" -Force
Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\Device Metadata" -Name "PreventDeviceMetadataFromNetwork" -Type DWord -Value 1
@@ -26,24 +51,30 @@ function Invoke-WPFUpdatessecurity {
Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\DriverSearching" -Name "DontSearchWindowsUpdate" -Type DWord -Value 1
Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\DriverSearching" -Name "DriverUpdateWizardWuSearchEnabled" -Type DWord -Value 0
New-Item -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" -Force
Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" -Name "ExcludeWUDriversInQualityUpdate" -Type DWord -Value 1
New-Item -Path $windowsUpdatePolicyPath -Force
Set-ItemProperty -Path $windowsUpdatePolicyPath -Name "ExcludeWUDriversInQualityUpdate" -Type DWord -Value 1
Write-Host "Setting cumulative updates back by 1 year and security updates by 4 days..."
Write-Host "Deferring feature updates by 365 days and quality updates by 4 days..."
Write-WinUtilLog -Component "Updates" -Message "Deferring feature updates by 365 days and quality updates by 4 days."
New-Item -Path "HKLM:\SOFTWARE\Microsoft\WindowsUpdate\UX\Settings" -Force
Set-ItemProperty -Path $windowsUpdatePolicyPath -Name "DeferFeatureUpdates" -Type DWord -Value 1
Set-ItemProperty -Path $windowsUpdatePolicyPath -Name "DeferFeatureUpdatesPeriodInDays" -Type DWord -Value 365
Set-ItemProperty -Path $windowsUpdatePolicyPath -Name "DeferQualityUpdates" -Type DWord -Value 1
Set-ItemProperty -Path $windowsUpdatePolicyPath -Name "DeferQualityUpdatesPeriodInDays" -Type DWord -Value 4
Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\WindowsUpdate\UX\Settings" -Name "BranchReadinessLevel" -Type DWord -Value 20
Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\WindowsUpdate\UX\Settings" -Name "DeferFeatureUpdatesPeriodInDays" -Type DWord -Value 365
Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\WindowsUpdate\UX\Settings" -Name "DeferQualityUpdatesPeriodInDays" -Type DWord -Value 4
$legacySettingsPath = "HKLM:\SOFTWARE\Microsoft\WindowsUpdate\UX\Settings"
foreach ($legacyValue in @("BranchReadinessLevel", "DeferFeatureUpdatesPeriodInDays", "DeferQualityUpdatesPeriodInDays")) {
Remove-ItemProperty -Path $legacySettingsPath -Name $legacyValue -ErrorAction SilentlyContinue
}
Write-Host "Disabling Windows Update automatic restart..."
Write-WinUtilLog -Component "Updates" -Message "Disabling Windows Update automatic restart while users are logged in."
Write-Host "Preventing automatic restarts while users are signed in..."
Write-WinUtilLog -Component "Updates" -Message "Configuring scheduled automatic updates without restarting while users are signed in."
New-Item -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" -Force
Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" -Name "NoAutoRebootWithLoggedOnUsers" -Type DWord -Value 1
Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" -Name "AUPowerManagement" -Type DWord -Value 0
New-Item -Path $automaticUpdatePolicyPath -Force
# NoAutoRebootWithLoggedOnUsers only applies when automatic updates use option 4.
Set-ItemProperty -Path $automaticUpdatePolicyPath -Name "AUOptions" -Type DWord -Value 4
Set-ItemProperty -Path $automaticUpdatePolicyPath -Name "NoAutoRebootWithLoggedOnUsers" -Type DWord -Value 1
Set-ItemProperty -Path $automaticUpdatePolicyPath -Name "AUPowerManagement" -Type DWord -Value 0
Write-Host "================================="
Write-Host "-- Updates Set to Recommended ---"
+4 -4
View File
@@ -39,13 +39,13 @@ function Invoke-WPFtweaksbutton {
Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -state "Normal" -value 0.01 -overlay "logo" }
}
Set-WinUtilProgressBar -Label "Creating restore point" -Percent 0
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "Creating restore point" -Percent 0
Write-WinUtilLog -Component "Tweaks" -Message "Creating restore point before applying selected tweaks."
Invoke-WinUtilTweaks $restorePointTweak
$completedSteps = 1
if ($tweaksToRun.Count -eq 0 -and $dnsProvider -eq "Default") {
Set-WinUtilProgressBar -Label "Tweaks finished" -Percent 100
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "Tweaks finished" -Percent 100
$sync.ProcessRunning = $false
Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -state "None" -overlay "checkmark" }
Write-Host "================================="
@@ -75,13 +75,13 @@ function Invoke-WPFtweaksbutton {
}
for ($i = 0; $i -lt $tweaks.Count; $i++) {
Set-WinUtilProgressBar -Label "Applying $($tweaks[$i])" -Percent ($completedSteps / $totalSteps * 100)
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "Applying $($tweaks[$i]) ($($completedSteps + 1)/$totalSteps)" -Percent ($completedSteps / $totalSteps * 100)
Invoke-WinUtilTweaks $tweaks[$i]
$completedSteps++
$progress = $completedSteps / $totalSteps
Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -value $progress }
}
Set-WinUtilProgressBar -Label "Tweaks finished" -Percent 100
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "Tweaks finished" -Percent 100
$sync.ProcessRunning = $false
Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -state "None" -overlay "checkmark" }
Write-Host "================================="
+2 -2
View File
@@ -33,12 +33,12 @@ function Invoke-WPFundoall {
for ($i = 0; $i -lt $tweaks.Count; $i++) {
Set-WinUtilProgressBar -Label "Undoing $($tweaks[$i])" -Percent ($i / $tweaks.Count * 100)
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "Undoing $($tweaks[$i]) ($($i + 1)/$($tweaks.Count))" -Percent ($i / $tweaks.Count * 100)
Invoke-WinUtiltweaks $tweaks[$i] -undo $true
Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -value ($i/$tweaks.Count) }
}
Set-WinUtilProgressBar -Label "Undo Tweaks Finished" -Percent 100
Set-WinUtilTweaksProgressIndicator -Visible $true -Label "Undo Tweaks Finished" -Percent 100
$sync.ProcessRunning = $false
Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -state "None" -overlay "checkmark" }
Write-Host "=================================="
+503 -23
View File
@@ -1,11 +1,38 @@
#===========================================================================
# Tests - AppX Removal
# Tests - AppX Management
#===========================================================================
BeforeAll {
$script:repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
. (Join-Path $script:repoRoot "functions\private\Get-WinUtilInstalledAPPX.ps1")
. (Join-Path $script:repoRoot "functions\private\Install-WinUtilAPPX.ps1")
. (Join-Path $script:repoRoot "functions\private\Remove-WinUtilAPPX.ps1")
. (Join-Path $script:repoRoot "functions\private\Remove-WinUtilProvisionedAPPX.ps1")
. (Join-Path $script:repoRoot "functions\public\Invoke-WPFAppxInstall.ps1")
. (Join-Path $script:repoRoot "functions\public\Invoke-WPFAppxRemoval.ps1")
. (Join-Path $script:repoRoot "functions\public\Invoke-WPFButton.ps1")
$tokens = $null
$parseErrors = $null
$provisionedSourcePath = Join-Path $script:repoRoot "functions\private\Remove-WinUtilProvisionedAPPX.ps1"
$provisionedSourceAst = [System.Management.Automation.Language.Parser]::ParseFile($provisionedSourcePath, [ref]$tokens, [ref]$parseErrors)
$ps5CommandAssignment = $provisionedSourceAst.Find({
param($node)
$node -is [System.Management.Automation.Language.AssignmentStatementAst] -and
$node.Left -is [System.Management.Automation.Language.VariableExpressionAst] -and
$node.Left.VariablePath.UserPath -eq "ps5Command"
}, $true)
$script:provisionedRemovalScriptBlock = $ps5CommandAssignment.Right.Expression.ScriptBlock.GetScriptBlock()
$installSourcePath = Join-Path $script:repoRoot "functions\private\Install-WinUtilAPPX.ps1"
$installSourceAst = [System.Management.Automation.Language.Parser]::ParseFile($installSourcePath, [ref]$tokens, [ref]$parseErrors)
$installCommandAssignment = $installSourceAst.Find({
param($node)
$node -is [System.Management.Automation.Language.AssignmentStatementAst] -and
$node.Left -is [System.Management.Automation.Language.VariableExpressionAst] -and
$node.Left.VariablePath.UserPath -eq "ps5Command"
}, $true)
$script:appxInstallScriptBlock = $installCommandAssignment.Right.Expression.ScriptBlock.GetScriptBlock()
function Write-WinUtilLog {
param($Message, $Level, $Component)
@@ -16,25 +43,46 @@ BeforeAll {
function Invoke-WPFRunspace {
param($ArgumentList, $ParameterList, [scriptblock]$ScriptBlock)
}
function Invoke-WPFUIThread {
param([scriptblock]$ScriptBlock)
}
function Set-WinUtilTweaksProgressIndicator {
param($Visible, $Label, $Percent)
}
function powershell.exe { }
function Get-AppxPackage {
param($Name, [switch]$AllUsers)
param($Name, [switch]$AllUsers, $ErrorAction)
}
function Add-AppxPackage {
param($Register, [switch]$DisableDevelopmentMode, $ErrorAction)
}
function Install-WinUtilWinget { }
function Install-WinUtilProgramWinget {
param($Action, $Programs)
}
function Remove-AppxPackage {
param(
[Parameter(ValueFromPipeline = $true)]
$InputObject,
[switch]$AllUsers
$Package,
[switch]$AllUsers,
$ErrorAction
)
process { }
}
function Remove-WinUtilProvisionedAPPX {
param($PackageList)
}
function Get-AppxProvisionedPackage {
param([switch]$Online)
param([switch]$Online, $ErrorAction)
}
function Remove-AppxProvisionedPackage {
param(
[Parameter(ValueFromPipeline = $true)]
$InputObject,
[switch]$Online
[switch]$Online,
$PackageName,
$ErrorAction
)
process { }
}
@@ -53,40 +101,360 @@ BeforeAll {
}
}
Describe "Get-WinUtilInstalledAPPX" {
BeforeEach {
Mock Write-WinUtilLog { }
Mock powershell.exe {
$global:LASTEXITCODE = 0
@("Example.One", "Example.Two")
}
}
It "queries installed package names through Windows PowerShell" {
$result = Get-WinUtilInstalledAPPX
$result | Should -Be @("Example.One", "Example.Two")
Should -Invoke -CommandName powershell.exe -Times 1 -Exactly
Should -Invoke -CommandName Write-WinUtilLog -Times 0 -Exactly
}
It "logs query failures and returns no package names" {
Mock powershell.exe {
$global:LASTEXITCODE = 1
"AppX query failed"
}
$result = @(Get-WinUtilInstalledAPPX)
$result | Should -HaveCount 0
Should -Invoke -CommandName Write-WinUtilLog -Times 1 -Exactly -ParameterFilter {
$Level -eq "ERROR" -and
$Component -eq "AppX" -and
$Message -eq "Failed to get installed AppX packages: AppX query failed"
}
}
}
Describe "Install-WinUtilAPPX" {
BeforeEach {
Mock Write-WinUtilLog { }
Mock Install-WinUtilWinget { }
Mock Install-WinUtilProgramWinget { }
Mock powershell.exe {
$global:LASTEXITCODE = 0
"C:\Program Files\WindowsApps\Example.Package\AppxManifest.xml"
}
}
It "uses a local manifest without contacting the Microsoft Store" {
Install-WinUtilAPPX -Name "Example.Package" -StoreId "9EXAMPLE1234"
Should -Invoke -CommandName Install-WinUtilWinget -Times 0 -Exactly
Should -Invoke -CommandName Install-WinUtilProgramWinget -Times 0 -Exactly
Should -Invoke -CommandName Write-WinUtilLog -Times 1 -Exactly -ParameterFilter {
$Component -eq "AppX" -and
$Message -like "Registered local AppX manifest for Example.Package*"
}
}
It "falls back to the Microsoft Store when no local manifest is available" {
Mock powershell.exe { $global:LASTEXITCODE = 0 }
Install-WinUtilAPPX -Name "Example.Package" -StoreId "9EXAMPLE1234"
Should -Invoke -CommandName Install-WinUtilWinget -Times 1 -Exactly
Should -Invoke -CommandName Install-WinUtilProgramWinget -Times 1 -Exactly -ParameterFilter {
$Action -eq "Install" -and $Programs.Count -eq 1 -and $Programs[0] -eq "msstore:9EXAMPLE1234"
}
}
It "logs local registration failures before using the Microsoft Store" {
Mock powershell.exe {
$global:LASTEXITCODE = 1
"Registration failed"
}
Install-WinUtilAPPX -Name "Example.Package" -StoreId "9EXAMPLE1234"
Should -Invoke -CommandName Write-WinUtilLog -Times 1 -Exactly -ParameterFilter {
$Level -eq "WARN" -and
$Component -eq "AppX" -and
$Message -eq "Local AppX registration failed for Example.Package: Registration failed"
}
Should -Invoke -CommandName Install-WinUtilProgramWinget -Times 1 -Exactly
}
It "throws after logging an error when neither install method is available" {
Mock powershell.exe { $global:LASTEXITCODE = 0 }
{ Install-WinUtilAPPX -Name "Example.Package" } |
Should -Throw "Unable to install Example.Package because no local manifest or Microsoft Store ID is available."
Should -Invoke -CommandName Install-WinUtilProgramWinget -Times 0 -Exactly
Should -Invoke -CommandName Write-WinUtilLog -Times 1 -Exactly -ParameterFilter {
$Level -eq "ERROR" -and
$Component -eq "AppX" -and
$Message -eq "Unable to install Example.Package because no local manifest or Microsoft Store ID is available."
}
}
It "registers an installed package manifest through Windows PowerShell" {
Mock Get-AppxPackage {
[pscustomobject]@{
InstallLocation = "C:\Program Files\WindowsApps\Example.Package"
Version = [version]"2.0.0.0"
}
}
Mock Get-AppxProvisionedPackage { }
Mock Test-Path { $LiteralPath -eq "C:\Program Files\WindowsApps\Example.Package\AppxManifest.xml" }
Mock Add-AppxPackage { }
$result = & $script:appxInstallScriptBlock "Example.Package"
$result | Should -Be "C:\Program Files\WindowsApps\Example.Package\AppxManifest.xml"
Should -Invoke -CommandName Get-AppxPackage -Times 1 -Exactly
Should -Invoke -CommandName Add-AppxPackage -Times 1 -Exactly
}
}
Describe "Get installed AppX selection" {
BeforeEach {
$script:sync = [Hashtable]::Synchronized(@{
ProcessRunning = $false
configs = @{
feature = @{}
appxHashtable = @{
WPFAppxExample = [pscustomobject]@{ PackageId = "Example.Package" }
WPFAppxMissing = [pscustomobject]@{ PackageId = "Missing.Package" }
}
}
WPFAppxExample = [pscustomobject]@{ IsChecked = $false }
WPFAppxMissing = [pscustomobject]@{ IsChecked = $false }
})
Mock Set-WinUtilTweaksProgressIndicator { }
Mock Get-WinUtilInstalledAPPX { @("Example.Package") }
Mock Invoke-WPFAppxInstall { }
}
AfterEach {
Remove-Variable -Name sync -Scope Script -ErrorAction SilentlyContinue
}
It "selects configured packages returned by the compatibility-safe query" {
Invoke-WPFButton -Button "WPFGetInstalledAppx"
Should -Invoke -CommandName Get-WinUtilInstalledAPPX -Times 1 -Exactly
$script:sync.WPFAppxExample.IsChecked | Should -BeTrue
$script:sync.WPFAppxMissing.IsChecked | Should -BeFalse
}
It "routes the install button to the AppX install workflow" {
Invoke-WPFButton -Button "WPFInstallSelectedAppx"
Should -Invoke -CommandName Invoke-WPFAppxInstall -Times 1 -Exactly
}
}
Describe "Remove-WinUtilAPPX" {
BeforeEach {
Mock Write-Host { }
Mock Write-WinUtilLog { }
Mock Get-AppxPackage {
[pscustomobject]@{
Name = $Name
}
@(
[pscustomobject]@{
Name = $Name
PackageFullName = "$Name.FullName"
}
[pscustomobject]@{
Name = $Name
PackageFullName = "$Name.FullName"
}
)
}
Mock Remove-AppxPackage { }
}
It "removes matching installed AppX packages" {
Remove-WinUtilAPPX -Name "Microsoft.Xbox*"
Should -Invoke -CommandName Get-AppxPackage -Times 1 -Exactly -ParameterFilter {
$Name -eq "*Microsoft.Xbox**" -and $AllUsers -eq $true
}
Should -Invoke -CommandName Remove-AppxPackage -Times 1 -Exactly -ParameterFilter {
$Package -eq "*Microsoft.Xbox**.FullName" -and
$AllUsers -eq $true -and
$ErrorAction -eq "Stop"
}
}
It "logs installed AppX removal failures" {
Mock Remove-AppxPackage { throw "Removal failed" }
{ Remove-WinUtilAPPX -Name "Example.Package" } | Should -Not -Throw
Should -Invoke -CommandName Write-WinUtilLog -Times 1 -Exactly -ParameterFilter {
$Level -eq "ERROR" -and
$Component -eq "AppX" -and
$Message -eq "Failed to remove AppX package *Example.Package*.FullName: Removal failed"
}
}
}
Describe "Remove-WinUtilProvisionedAPPX" {
BeforeEach {
Mock Get-AppxProvisionedPackage {
@(
[pscustomobject]@{ DisplayName = "Microsoft.XboxGamingOverlay" }
[pscustomobject]@{ DisplayName = "Microsoft.WindowsCalculator" }
[pscustomobject]@{ DisplayName = "Example.One"; PackageName = "Example.One_1.0" }
[pscustomobject]@{ DisplayName = "Example.Two"; PackageName = "Example.Two_1.0" }
)
}
Mock Remove-AppxProvisionedPackage { }
}
It "removes matching installed and provisioned AppX packages" {
Remove-WinUtilAPPX -Name "Microsoft.Xbox*"
It "queries provisioned packages once for all selected package names" {
& $script:provisionedRemovalScriptBlock "Example.One" "Example.Two"
Should -Invoke -CommandName Get-AppxPackage -Times 1 -Exactly -ParameterFilter {
$Name -eq "Microsoft.Xbox*" -and $AllUsers -eq $true
}
Should -Invoke -CommandName Remove-AppxPackage -Times 1 -Exactly -ParameterFilter {
$InputObject.Name -eq "Microsoft.Xbox*" -and $AllUsers -eq $true
}
Should -Invoke -CommandName Get-AppxProvisionedPackage -Times 1 -Exactly -ParameterFilter {
$Online -eq $true
}
Should -Invoke -CommandName Remove-AppxProvisionedPackage -Times 1 -Exactly -ParameterFilter {
$InputObject.DisplayName -eq "Microsoft.XboxGamingOverlay" -and $Online -eq $true
$PackageName -eq "Example.One_1.0" -and $Online -eq $true
}
Should -Invoke -CommandName Remove-AppxProvisionedPackage -Times 1 -Exactly -ParameterFilter {
$PackageName -eq "Example.Two_1.0" -and $Online -eq $true
}
}
It "surfaces provisioned package removal failures from the child process" {
Mock Remove-AppxProvisionedPackage { throw "DISM failed" }
{ & $script:provisionedRemovalScriptBlock "Example.One" } |
Should -Throw "*Failed to remove provisioned AppX package Example.One_1.0: DISM failed*"
Should -Invoke -CommandName Remove-AppxProvisionedPackage -Times 1 -Exactly -ParameterFilter {
$PackageName -eq "Example.One_1.0" -and
$Online -eq $true -and
$ErrorAction -eq "Stop"
}
}
It "throws after logging child process failures" {
$source = Get-Content -Path $provisionedSourcePath -Raw
$source | Should -Match '\$removalOutput = powershell\.exe .* 2>&1'
$source | Should -Match 'Write-WinUtilLog -Level "ERROR" -Component "AppX" -Message \$errorMessage'
$source | Should -Match '(?s)AppX provisioned package removal failed:.*throw \$errorMessage.*AppX provisioned package removal completed\.'
}
}
Describe "Invoke-WPFAppxInstall" {
BeforeEach {
$script:sync = [Hashtable]::Synchronized(@{
ProcessRunning = $false
Form = [pscustomobject]@{ Dispatcher = [pscustomobject]@{} }
selectedAppx = [System.Collections.Generic.List[string]]::new()
configs = @{
appxHashtable = @{
WPFAppxExample = [pscustomobject]@{
Content = "Example App"
PackageId = "Example.Package"
StoreId = "9EXAMPLE1234"
}
}
}
})
$script:capturedAppxInstallScriptBlock = $null
$script:capturedAppxInstallParameterList = $null
$script:appxInstallProcessRunningAtLaunch = $null
Mock Show-WinUtilMessage { "OK" }
Mock Write-Host { }
Mock Write-WinUtilLog { }
Mock Set-WinUtilTweaksProgressIndicator { }
Mock Invoke-WPFUIThread { }
Mock Install-WinUtilAPPX { }
Mock Invoke-WPFRunspace {
$script:appxInstallProcessRunningAtLaunch = $script:sync.ProcessRunning
$script:capturedAppxInstallScriptBlock = $ScriptBlock
$script:capturedAppxInstallParameterList = $ParameterList
[pscustomobject]@{ MockHandle = $true }
}
}
AfterEach {
Remove-Variable -Name sync -Scope Script -ErrorAction SilentlyContinue
Remove-Variable -Name capturedAppxInstallScriptBlock -Scope Script -ErrorAction SilentlyContinue
Remove-Variable -Name capturedAppxInstallParameterList -Scope Script -ErrorAction SilentlyContinue
Remove-Variable -Name appxInstallProcessRunningAtLaunch -Scope Script -ErrorAction SilentlyContinue
}
It "prompts and exits when no AppX packages are selected for install" {
Invoke-WPFAppxInstall
Should -Invoke -CommandName Show-WinUtilMessage -Times 1 -Exactly -ParameterFilter {
$Message -eq "No AppX Package selected" -and
$Title -eq "Error" -and
$Button -eq "OK" -and
$Icon -eq "Error"
}
Should -Invoke -CommandName Invoke-WPFRunspace -Times 0 -Exactly
}
It "prevents overlapping AppX install operations" {
$script:sync.ProcessRunning = $true
$script:sync.selectedAppx.Add("WPFAppxExample")
Invoke-WPFAppxInstall
Should -Invoke -CommandName Show-WinUtilMessage -Times 1 -Exactly -ParameterFilter {
$Message -eq "An AppX process is currently running." -and
$Title -eq "WinUtil" -and
$Button -eq "OK" -and
$Icon -eq "Warning"
}
Should -Invoke -CommandName Invoke-WPFRunspace -Times 0 -Exactly
}
It "installs selected AppX packages with their Store IDs" {
$script:sync.selectedAppx.Add("WPFAppxExample")
Invoke-WPFAppxInstall
$script:appxInstallProcessRunningAtLaunch | Should -BeTrue
& $script:capturedAppxInstallScriptBlock -selected @("WPFAppxExample") -apps $script:sync.configs.appxHashtable
$script:capturedAppxInstallParameterList[0][1][0] | Should -Be "WPFAppxExample"
Should -Invoke -CommandName Install-WinUtilAPPX -Times 1 -Exactly -ParameterFilter {
$Name -eq "Example.Package" -and $StoreId -eq "9EXAMPLE1234"
}
Should -Invoke -CommandName Set-WinUtilTweaksProgressIndicator -Times 1 -Exactly -ParameterFilter {
$Visible -eq $true -and $Label -eq "Installing Example App (1/1)" -and $Percent -eq 0
}
Should -Invoke -CommandName Set-WinUtilTweaksProgressIndicator -Times 1 -Exactly -ParameterFilter {
$Visible -eq $true -and $Label -eq "Installed Example App (1/1)" -and $Percent -eq 100
}
Should -Invoke -CommandName Set-WinUtilTweaksProgressIndicator -Times 1 -Exactly -ParameterFilter {
$Visible -eq $true -and $Label -eq "AppX install finished" -and $Percent -eq 100
}
Should -Invoke -CommandName Invoke-WPFUIThread -Times 1 -Exactly -ParameterFilter {
$ScriptBlock.ToString() -like '*Set-WinUtilTaskbaritem -state "None" -overlay "checkmark"*'
}
$script:sync.ProcessRunning | Should -BeFalse
}
It "shows failure feedback and clears ProcessRunning when install fails" {
$script:sync.selectedAppx.Add("WPFAppxExample")
Mock Install-WinUtilAPPX { throw "Install failed" }
Invoke-WPFAppxInstall
& $script:capturedAppxInstallScriptBlock -selected @("WPFAppxExample") -apps $script:sync.configs.appxHashtable
Should -Invoke -CommandName Set-WinUtilTweaksProgressIndicator -Times 1 -Exactly -ParameterFilter {
$Visible -eq $true -and $Label -eq "AppX install failed" -and $Percent -eq 100
}
Should -Invoke -CommandName Invoke-WPFUIThread -Times 1 -Exactly -ParameterFilter {
$ScriptBlock.ToString() -like '*Set-WinUtilTaskbaritem -state "Error" -overlay "warning"*'
}
$script:sync.ProcessRunning | Should -BeFalse
}
}
@@ -101,9 +469,11 @@ Describe "Invoke-WPFAppxRemoval entrypoint" {
})
$script:capturedAppxScriptBlock = $null
$script:capturedAppxParameterList = $null
$script:appxRemovalProcessRunningAtLaunch = $null
Mock Show-WinUtilMessage { "OK" }
Mock Invoke-WPFRunspace {
$script:appxRemovalProcessRunningAtLaunch = $script:sync.ProcessRunning
$script:capturedAppxScriptBlock = $ScriptBlock
$script:capturedAppxParameterList = $ParameterList
[pscustomobject]@{ MockHandle = $true }
@@ -114,6 +484,7 @@ Describe "Invoke-WPFAppxRemoval entrypoint" {
Remove-Variable -Name sync -Scope Script -ErrorAction SilentlyContinue
Remove-Variable -Name capturedAppxScriptBlock -Scope Script -ErrorAction SilentlyContinue
Remove-Variable -Name capturedAppxParameterList -Scope Script -ErrorAction SilentlyContinue
Remove-Variable -Name appxRemovalProcessRunningAtLaunch -Scope Script -ErrorAction SilentlyContinue
}
It "prompts and exits when no AppX packages are selected" {
@@ -128,6 +499,21 @@ Describe "Invoke-WPFAppxRemoval entrypoint" {
Should -Invoke -CommandName Invoke-WPFRunspace -Times 0 -Exactly
}
It "prevents overlapping AppX removal operations" {
$script:sync.ProcessRunning = $true
$script:sync.selectedAppx.Add("WPFAppxExample")
Invoke-WPFAppxRemoval
Should -Invoke -CommandName Show-WinUtilMessage -Times 1 -Exactly -ParameterFilter {
$Message -eq "An AppX process is currently running." -and
$Title -eq "WinUtil" -and
$Button -eq "OK" -and
$Icon -eq "Warning"
}
Should -Invoke -CommandName Invoke-WPFRunspace -Times 0 -Exactly
}
It "passes selected AppX keys and app metadata to the removal runspace" {
$script:sync.selectedAppx.Add("WPFAppxExample")
$script:sync.configs.appxHashtable["WPFAppxExample"] = [pscustomobject]@{
@@ -136,7 +522,10 @@ Describe "Invoke-WPFAppxRemoval entrypoint" {
}
Invoke-WPFAppxRemoval
$script:sync.selectedAppx.Add("WPFAppxChangedAfterLaunch")
$script:appxRemovalProcessRunningAtLaunch | Should -BeTrue
$script:capturedAppxParameterList[0][1] | Should -HaveCount 1
Should -Invoke -CommandName Show-WinUtilMessage -Times 0 -Exactly
Should -Invoke -CommandName Invoke-WPFRunspace -Times 1 -Exactly -ParameterFilter {
$ScriptBlock -is [scriptblock] -and
@@ -153,6 +542,7 @@ Describe "Invoke-WPFAppxRemoval runspace body" {
BeforeEach {
$script:sync = [Hashtable]::Synchronized(@{
ProcessRunning = $false
Form = [pscustomobject]@{ Dispatcher = [pscustomobject]@{} }
selectedAppx = [System.Collections.Generic.List[string]]::new()
configs = @{
appxHashtable = @{}
@@ -185,14 +575,18 @@ Describe "Invoke-WPFAppxRemoval runspace body" {
Mock Show-WinUtilMessage { "OK" }
Mock Write-Host { }
Mock Write-WinUtilLog { }
Mock Set-WinUtilTweaksProgressIndicator { }
Mock Invoke-WPFUIThread { }
Mock Stop-Process { }
Mock Set-ItemProperty { }
Mock Get-AppxPackage {
[pscustomobject]@{
Name = $Name
PackageFullName = "$Name.FullName"
}
}
Mock Remove-AppxPackage { }
Mock Remove-WinUtilProvisionedAPPX { }
Mock Get-Package {
[pscustomobject]@{
Name = "Microsoft Teams Meeting Add-in"
@@ -216,10 +610,84 @@ Describe "Invoke-WPFAppxRemoval runspace body" {
& $script:capturedAppxScriptBlock -selected $selected -apps $script:apps
Should -Invoke -CommandName Get-AppxPackage -Times 1 -Exactly -ParameterFilter {
$Name -eq "Example.Package" -and $AllUsers -eq $true
$Name -eq "*Example.Package*" -and $AllUsers -eq $true
}
Should -Invoke -CommandName Remove-AppxPackage -Times 1 -Exactly -ParameterFilter {
$InputObject.Name -eq "Example.Package" -and $AllUsers -eq $true
$Package -eq "*Example.Package*.FullName"
}
Should -Invoke -CommandName Remove-WinUtilProvisionedAPPX -Times 1 -Exactly -ParameterFilter {
$PackageList.Count -eq 1 -and $PackageList[0] -eq "Example.Package"
}
Should -Invoke -CommandName Set-WinUtilTweaksProgressIndicator -Times 1 -Exactly -ParameterFilter {
$Visible -eq $true -and $Label -eq "Removing Example App (1/1)" -and $Percent -eq 0
}
Should -Invoke -CommandName Set-WinUtilTweaksProgressIndicator -Times 1 -Exactly -ParameterFilter {
$Visible -eq $true -and $Label -eq "Removed Example App (1/1)" -and $Percent -eq 90
}
Should -Invoke -CommandName Set-WinUtilTweaksProgressIndicator -Times 1 -Exactly -ParameterFilter {
$Visible -eq $true -and $Label -eq "AppX removal finished" -and $Percent -eq 100
}
Should -Invoke -CommandName Invoke-WPFUIThread -Times 1 -Exactly -ParameterFilter {
$ScriptBlock.ToString() -like '*Set-WinUtilTaskbaritem -state "None" -overlay "checkmark"*'
}
$script:sync.ProcessRunning | Should -BeFalse
}
It "shows failure feedback and clears ProcessRunning when removal fails" {
$selected = @("WPFAppxExample")
$script:sync.selectedAppx.Add("WPFAppxExample")
$script:sync.configs.appxHashtable = $script:apps
Mock Remove-WinUtilAPPX { throw "Removal failed" }
Invoke-WPFAppxRemoval
& $script:capturedAppxScriptBlock -selected $selected -apps $script:apps
Should -Invoke -CommandName Set-WinUtilTweaksProgressIndicator -Times 1 -Exactly -ParameterFilter {
$Visible -eq $true -and $Label -eq "AppX removal failed" -and $Percent -eq 100
}
Should -Invoke -CommandName Invoke-WPFUIThread -Times 1 -Exactly -ParameterFilter {
$ScriptBlock.ToString() -like '*Set-WinUtilTaskbaritem -state "Error" -overlay "warning"*'
}
Should -Invoke -CommandName Remove-WinUtilProvisionedAPPX -Times 0 -Exactly
$script:sync.ProcessRunning | Should -BeFalse
}
It "removes packages without UI progress during headless autorun" {
$selected = @("WPFAppxExample")
$script:sync.Remove("Form")
$script:sync.selectedAppx.Add("WPFAppxExample")
$script:sync.configs.appxHashtable = $script:apps
Invoke-WPFAppxRemoval
& $script:capturedAppxScriptBlock -selected $selected -apps $script:apps
Should -Invoke -CommandName Remove-AppxPackage -Times 1 -Exactly
Should -Invoke -CommandName Remove-WinUtilProvisionedAPPX -Times 1 -Exactly
Should -Invoke -CommandName Set-WinUtilTweaksProgressIndicator -Times 0 -Exactly
Should -Invoke -CommandName Invoke-WPFUIThread -Times 0 -Exactly
$script:sync.ProcessRunning | Should -BeFalse
}
It "shows failure feedback when provisioned package removal fails" {
$selected = @("WPFAppxExample")
$script:sync.selectedAppx.Add("WPFAppxExample")
$script:sync.configs.appxHashtable = $script:apps
Mock Remove-WinUtilProvisionedAPPX { throw "Provisioned removal failed" }
Invoke-WPFAppxRemoval
& $script:capturedAppxScriptBlock -selected $selected -apps $script:apps
Should -Invoke -CommandName Set-WinUtilTweaksProgressIndicator -Times 1 -Exactly -ParameterFilter {
$Visible -eq $true -and $Label -eq "AppX removal failed" -and $Percent -eq 100
}
Should -Invoke -CommandName Set-WinUtilTweaksProgressIndicator -Times 0 -Exactly -ParameterFilter {
$Label -eq "AppX removal finished"
}
Should -Invoke -CommandName Invoke-WPFUIThread -Times 1 -Exactly -ParameterFilter {
$ScriptBlock.ToString() -like '*Set-WinUtilTaskbaritem -state "Error" -overlay "warning"*'
}
Should -Invoke -CommandName Invoke-WPFUIThread -Times 0 -Exactly -ParameterFilter {
$ScriptBlock.ToString() -like '*Set-WinUtilTaskbaritem -state "None" -overlay "checkmark"*'
}
$script:sync.ProcessRunning | Should -BeFalse
}
@@ -239,7 +707,10 @@ Describe "Invoke-WPFAppxRemoval runspace body" {
& $script:capturedAppxScriptBlock -selected $selected -apps $script:apps
Should -Invoke -CommandName Stop-Process -Times 1 -Exactly -ParameterFilter {
$Name -eq "GameBarFTServer"
$Name -eq "GameBarFTServer" -and
$Force -eq $true -and
$Confirm -eq $false -and
$ErrorAction -eq "SilentlyContinue"
}
Should -Invoke -CommandName Set-ItemProperty -Times 1 -Exactly -ParameterFilter {
$Path -eq "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\GameDVR" -and
@@ -247,7 +718,10 @@ Describe "Invoke-WPFAppxRemoval runspace body" {
$Value -eq 0
}
Should -Invoke -CommandName Stop-Process -Times 1 -Exactly -ParameterFilter {
$Name -eq "dllhost"
$Name -eq "dllhost" -and
$Force -eq $true -and
$Confirm -eq $false -and
$ErrorAction -eq "SilentlyContinue"
}
Should -Invoke -CommandName Get-Package -Times 1 -Exactly -ParameterFilter {
$Name -eq "Microsoft Teams*"
@@ -256,6 +730,12 @@ Describe "Invoke-WPFAppxRemoval runspace body" {
$InputObject.Name -eq "Microsoft Teams Meeting Add-in" -and $Force -eq $true
}
Should -Invoke -CommandName Remove-AppxPackage -Times 3 -Exactly
Should -Invoke -CommandName Remove-WinUtilProvisionedAPPX -Times 1 -Exactly -ParameterFilter {
$PackageList.Count -eq 3 -and
$PackageList[0] -eq "Microsoft.XboxGamingOverlay" -and
$PackageList[1] -eq "Microsoft.WindowsNotepad" -and
$PackageList[2] -eq "MSTeams"
}
$script:sync.ProcessRunning | Should -BeFalse
}
}
+5
View File
@@ -326,6 +326,11 @@ Describe "UI-rendered config entries" {
foreach ($missingField in (Get-WinUtilMissingRequiredFields -EntryName $entry.Name -Entry $entry.Value -RequiredFields $requiredFields)) {
$invalidEntries.Add($missingField)
}
if ((Test-WinUtilHasProperty -Object $entry.Value -Name "StoreId") -and
$entry.Value.StoreId -notmatch '^(?:[A-Z0-9]{12}|XP[A-Z0-9]{12})$') {
$invalidEntries.Add("$($entry.Name) has invalid Microsoft Store ID '$($entry.Value.StoreId)'")
}
}
if ($invalidEntries.Count -gt 0) {
+77 -3
View File
@@ -23,6 +23,20 @@ BeforeAll {
param($Message, $Level, $Component)
}
function Add-DnsClientDohServerAddress {
param($ServerAddress, $DohTemplate, $AllowFallbackToUdp, $AutoUpgrade, $ErrorAction)
}
function Set-DnsClientDohServerAddress {
param($ServerAddress, $DohTemplate, $AllowFallbackToUdp, $AutoUpgrade, $ErrorAction)
}
function Get-DnsClientDohServerAddress {
param($ServerAddress, $ErrorAction)
}
function Remove-DnsClientDohServerAddress {
param($ServerAddress, $Confirm, $ErrorAction)
}
function Clear-DnsClientCache { }
. (Join-Path $script:repoRoot "functions\private\Set-WinUtilDNS.ps1")
}
@@ -36,6 +50,7 @@ Describe "Set-WinUtilDNS" {
Secondary = "1.0.0.1"
Primary6 = "2606:4700:4700::1111"
Secondary6 = "2606:4700:4700::1001"
DohTemplate = "https://cloudflare-dns.com/dns-query"
}
}
}
@@ -46,6 +61,7 @@ Describe "Set-WinUtilDNS" {
Name = "Ethernet"
Status = "Up"
ifIndex = 7
InterfaceGuid = "{1234-5678-90AB-CDEF}"
}
}
Mock Set-DnsClientServerAddress { }
@@ -53,13 +69,25 @@ Describe "Set-WinUtilDNS" {
Mock Write-WinUtilLog { }
Mock Write-Warning { }
Mock Write-Host { }
Mock Get-Command { return $true } -ParameterFilter { $Name -eq "Add-DnsClientDohServerAddress" }
Mock Add-DnsClientDohServerAddress { }
Mock Set-DnsClientDohServerAddress { }
Mock Get-DnsClientDohServerAddress { return $null }
Mock Remove-DnsClientDohServerAddress { }
Mock Test-Path { return $false }
Mock Get-ChildItem { }
Mock New-Item { }
Mock New-ItemProperty { }
Mock Remove-Item { }
Mock Clear-DnsClientCache { }
}
AfterEach {
Remove-Variable -Name sync -Scope Script -ErrorAction SilentlyContinue
}
It "sets IPv4 and IPv6 DNS server addresses separately" {
It "sets IPv4 and IPv6 DNS server addresses separately and applies DoH templates" {
Set-WinUtilDNS -DNSProvider "Cloudflare"
Should -Invoke -CommandName Set-DnsClientServerAddress -Times 1 -Exactly -ParameterFilter {
@@ -77,9 +105,51 @@ Describe "Set-WinUtilDNS" {
Should -Invoke -CommandName Set-DnsClientServerAddress -Times 0 -Exactly -ParameterFilter {
$ServerAddresses.Count -eq 4
}
Should -Invoke -CommandName Add-DnsClientDohServerAddress -Times 4 -Exactly
Should -Invoke -CommandName New-ItemProperty -Times 4 -ParameterFilter {
$Name -eq "DohFlags" -and $Value -eq 1
}
Should -Invoke -CommandName Clear-DnsClientCache -Times 1 -Exactly
}
It "resets DNS to DHCP for IPv4 and IPv6" {
It "updates an existing DoH entry with the selected provider settings" {
Mock Get-DnsClientDohServerAddress {
if ($ServerAddress -eq "1.1.1.1") {
return [pscustomobject]@{ ServerAddress = $ServerAddress }
}
return $null
}
Set-WinUtilDNS -DNSProvider "Cloudflare"
Should -Invoke -CommandName Set-DnsClientDohServerAddress -Times 1 -Exactly -ParameterFilter {
$ServerAddress -eq "1.1.1.1" -and
$DohTemplate -eq "https://cloudflare-dns.com/dns-query" -and
$AllowFallbackToUdp -eq $false -and
$AutoUpgrade -eq $true
}
Should -Invoke -CommandName Add-DnsClientDohServerAddress -Times 3 -Exactly
}
It "resets DNS to DHCP and removes the applied DoH configuration" {
Mock Test-Path { return $true } -ParameterFilter { $Path -like "*DohInterfaceSettings*" }
Mock Get-ChildItem {
if ($Path -like "*\Doh6") {
return @(
[pscustomobject]@{ PSChildName = "2606:4700:4700::1111" }
[pscustomobject]@{ PSChildName = "2606:4700:4700::1001" }
)
}
return @(
[pscustomobject]@{ PSChildName = "1.1.1.1" }
[pscustomobject]@{ PSChildName = "1.0.0.1" }
)
}
Mock Get-DnsClientDohServerAddress {
[pscustomobject]@{ ServerAddress = $ServerAddress }
}
Set-WinUtilDNS -DNSProvider "DHCP"
Should -Invoke -CommandName Set-DnsClientServerAddress -Times 1 -Exactly -ParameterFilter {
@@ -102,6 +172,10 @@ Describe "Set-WinUtilDNS" {
$Arguments[4] -eq "name=Ethernet" -and
$Arguments[5] -eq "source=dhcp"
}
Should -Invoke -CommandName Remove-Item -Times 1 -Exactly -ParameterFilter {
$Path -like "*DohInterfaceSettings*" -and $Recurse -eq $true -and $Force -eq $true
}
Should -Invoke -CommandName Remove-DnsClientDohServerAddress -Times 4 -Exactly
}
It "catches DNS setter failures so the tweak runspace can continue" {
@@ -112,7 +186,7 @@ Describe "Set-WinUtilDNS" {
Should -Invoke -CommandName Write-WinUtilLog -Times 1 -Exactly -ParameterFilter {
$Level -eq "ERROR" -and
$Component -eq "DNS" -and
$Message -like "Unable to set DNS provider Cloudflare*"
$Message -like "DNS provider Cloudflare was not completed: *"
}
}
}
+2 -1
View File
@@ -21,6 +21,7 @@ Describe "Install app rendering startup contract" {
$renderScript | Should -Match 'Dispatcher\.BeginInvoke'
$renderScript | Should -Match 'Invoke-WinUtilInstallAppRenderNextBatch'
$renderScript | Should -Match 'Initialize-InstallAppEntry'
$renderScript | Should -Match 'Find-AppsByNameOrDescription -SearchString \$sync\.SearchBar\.Text -Category \$sync\.SearchBar\.Tag'
$renderScript | Should -Match '\$sync\.InstallAppEntriesRendered = \$true'
}
@@ -59,7 +60,7 @@ Describe "Install app rendering startup contract" {
}
function global:Find-AppsByNameOrDescription {
param($SearchString)
param($SearchString, $Category)
throw "Search should not run for an empty search box in this test."
}
+61 -17
View File
@@ -18,10 +18,9 @@ BeforeAll {
function Get-WinUtilSelectedPackages {
param($PackageList, [string]$Preference)
}
function Show-WPFInstallAppBusy {
param($text)
function Set-WinUtilTweaksProgressIndicator {
param($Visible, $Label, $Percent)
}
function Hide-WPFInstallAppBusy { }
function Install-WinUtilWinget { }
function Install-WinUtilChoco { }
function Install-WinUtilProgramWinget {
@@ -74,6 +73,9 @@ BeforeAll {
preferences = [pscustomobject]@{
packagemanager = "Winget"
}
Form = [pscustomobject]@{
Dispatcher = [pscustomobject]@{}
}
configs = @{
applicationsHashtable = $applications
}
@@ -187,8 +189,7 @@ Describe "Invoke-WPFInstall runspace body" {
Mock Get-WinUtilSelectedPackages {
New-WinUtilPackageSplit -Winget @("Git.Git") -Choco @("vlc")
}
Mock Show-WPFInstallAppBusy { }
Mock Hide-WPFInstallAppBusy { }
Mock Set-WinUtilTweaksProgressIndicator { }
Mock Install-WinUtilWinget { }
Mock Install-WinUtilChoco { }
Mock Install-WinUtilProgramWinget { }
@@ -212,8 +213,17 @@ Describe "Invoke-WPFInstall runspace body" {
Should -Invoke -CommandName Get-WinUtilSelectedPackages -Times 1 -Exactly -ParameterFilter {
@($PackageList).Count -eq 1 -and $Preference -eq "Winget"
}
Should -Invoke -CommandName Show-WPFInstallAppBusy -Times 1 -Exactly -ParameterFilter {
$text -eq "Installing apps..."
Should -Invoke -CommandName Set-WinUtilTweaksProgressIndicator -Times 1 -Exactly -ParameterFilter {
$Visible -eq $true -and $Label -eq "Preparing app install (0/2)" -and $Percent -eq 0
}
Should -Invoke -CommandName Set-WinUtilTweaksProgressIndicator -Times 1 -Exactly -ParameterFilter {
$Visible -eq $true -and $Label -eq "Installed Git.Git (1/2)" -and $Percent -eq 50
}
Should -Invoke -CommandName Set-WinUtilTweaksProgressIndicator -Times 1 -Exactly -ParameterFilter {
$Visible -eq $true -and $Label -eq "Installed Chocolatey packages (2/2)" -and $Percent -eq 100
}
Should -Invoke -CommandName Set-WinUtilTweaksProgressIndicator -Times 1 -Exactly -ParameterFilter {
$Visible -eq $true -and $Label -eq "App install finished" -and $Percent -eq 100
}
Should -Invoke -CommandName Install-WinUtilWinget -Times 1 -Exactly
Should -Invoke -CommandName Install-WinUtilProgramWinget -Times 1 -Exactly -ParameterFilter {
@@ -223,21 +233,34 @@ Describe "Invoke-WPFInstall runspace body" {
Should -Invoke -CommandName Install-WinUtilProgramChoco -Times 1 -Exactly -ParameterFilter {
$Action -eq "Install" -and @($Programs)[0] -eq "vlc"
}
Should -Invoke -CommandName Hide-WPFInstallAppBusy -Times 1 -Exactly
Should -Invoke -CommandName Invoke-WPFUIThread -Times 1 -Exactly -ParameterFilter {
$ScriptBlock.ToString() -like '*$sync.ItemsControl.IsEnabled = $false*'
}
Should -Invoke -CommandName Invoke-WPFUIThread -Times 1 -Exactly -ParameterFilter {
$ScriptBlock.ToString() -like '*$sync.ItemsControl.IsEnabled = $true*'
}
Should -Invoke -CommandName Invoke-WPFUIThread -Times 1 -Exactly -ParameterFilter {
$ScriptBlock.ToString() -like '*Set-WinUtilTaskbaritem -state "None" -overlay "checkmark"*'
}
$script:sync.ProcessRunning | Should -BeFalse
}
It "hides the busy overlay, sets taskbar error state, and clears ProcessRunning on failure" {
It "shows failure progress, sets taskbar error state, and clears ProcessRunning on failure" {
Mock Install-WinUtilProgramWinget { throw "winget failed" }
Invoke-WPFInstall
& $script:capturedInstallScriptBlock -PackagesToInstall @($script:package) -ManagerPreference "Winget"
Should -Invoke -CommandName Hide-WPFInstallAppBusy -Times 1 -Exactly
Should -Invoke -CommandName Set-WinUtilTweaksProgressIndicator -Times 1 -Exactly -ParameterFilter {
$Visible -eq $true -and $Label -eq "App install failed" -and $Percent -eq 100
}
Should -Invoke -CommandName Invoke-WPFUIThread -Times 1 -Exactly -ParameterFilter {
$ScriptBlock.ToString() -like '*$sync.ItemsControl.IsEnabled = $false*'
}
Should -Invoke -CommandName Invoke-WPFUIThread -Times 1 -Exactly -ParameterFilter {
$ScriptBlock.ToString() -like '*$sync.ItemsControl.IsEnabled = $true*'
}
Should -Invoke -CommandName Invoke-WPFUIThread -Times 1 -Exactly -ParameterFilter {
$ScriptBlock.ToString() -like '*Set-WinUtilTaskbaritem -state "Error" -overlay "warning"*'
}
@@ -345,8 +368,7 @@ Describe "Invoke-WPFUnInstall runspace body" {
Mock Get-WinUtilSelectedPackages {
New-WinUtilPackageSplit -Winget @("Git.Git") -Choco @("vlc")
}
Mock Show-WPFInstallAppBusy { }
Mock Hide-WPFInstallAppBusy { }
Mock Set-WinUtilTweaksProgressIndicator { }
Mock Install-WinUtilProgramWinget { }
Mock Install-WinUtilProgramChoco { }
Mock Invoke-WPFUIThread { }
@@ -369,8 +391,17 @@ Describe "Invoke-WPFUnInstall runspace body" {
Should -Invoke -CommandName Get-WinUtilSelectedPackages -Times 1 -Exactly -ParameterFilter {
@($PackageList).Count -eq 1 -and $Preference -eq "Winget"
}
Should -Invoke -CommandName Show-WPFInstallAppBusy -Times 1 -Exactly -ParameterFilter {
$text -eq "Uninstalling apps..."
Should -Invoke -CommandName Set-WinUtilTweaksProgressIndicator -Times 1 -Exactly -ParameterFilter {
$Visible -eq $true -and $Label -eq "Preparing app uninstall (0/2)" -and $Percent -eq 0
}
Should -Invoke -CommandName Set-WinUtilTweaksProgressIndicator -Times 1 -Exactly -ParameterFilter {
$Visible -eq $true -and $Label -eq "Uninstalled Git.Git (1/2)" -and $Percent -eq 50
}
Should -Invoke -CommandName Set-WinUtilTweaksProgressIndicator -Times 1 -Exactly -ParameterFilter {
$Visible -eq $true -and $Label -eq "Uninstalled Chocolatey packages (2/2)" -and $Percent -eq 100
}
Should -Invoke -CommandName Set-WinUtilTweaksProgressIndicator -Times 1 -Exactly -ParameterFilter {
$Visible -eq $true -and $Label -eq "App uninstall finished" -and $Percent -eq 100
}
Should -Invoke -CommandName Install-WinUtilProgramWinget -Times 1 -Exactly -ParameterFilter {
$Action -eq "Uninstall" -and @($Programs)[0] -eq "Git.Git"
@@ -378,21 +409,34 @@ Describe "Invoke-WPFUnInstall runspace body" {
Should -Invoke -CommandName Install-WinUtilProgramChoco -Times 1 -Exactly -ParameterFilter {
$Action -eq "Uninstall" -and @($Programs)[0] -eq "vlc"
}
Should -Invoke -CommandName Hide-WPFInstallAppBusy -Times 1 -Exactly
Should -Invoke -CommandName Invoke-WPFUIThread -Times 1 -Exactly -ParameterFilter {
$ScriptBlock.ToString() -like '*$sync.ItemsControl.IsEnabled = $false*'
}
Should -Invoke -CommandName Invoke-WPFUIThread -Times 1 -Exactly -ParameterFilter {
$ScriptBlock.ToString() -like '*$sync.ItemsControl.IsEnabled = $true*'
}
Should -Invoke -CommandName Invoke-WPFUIThread -Times 1 -Exactly -ParameterFilter {
$ScriptBlock.ToString() -like '*Set-WinUtilTaskbaritem -state "None" -overlay "checkmark"*'
}
$script:sync.ProcessRunning | Should -BeFalse
}
It "hides the busy overlay, sets taskbar error state, and clears ProcessRunning on failure" {
It "shows failure progress, sets taskbar error state, and clears ProcessRunning on failure" {
Mock Install-WinUtilProgramWinget { throw "winget failed" }
Invoke-WPFUnInstall -PackagesToUninstall @($script:package)
& $script:capturedUninstallScriptBlock -PackagesToUninstall @($script:package) -ManagerPreference "Winget"
Should -Invoke -CommandName Hide-WPFInstallAppBusy -Times 1 -Exactly
Should -Invoke -CommandName Set-WinUtilTweaksProgressIndicator -Times 1 -Exactly -ParameterFilter {
$Visible -eq $true -and $Label -eq "App uninstall failed" -and $Percent -eq 100
}
Should -Invoke -CommandName Invoke-WPFUIThread -Times 1 -Exactly -ParameterFilter {
$ScriptBlock.ToString() -like '*$sync.ItemsControl.IsEnabled = $false*'
}
Should -Invoke -CommandName Invoke-WPFUIThread -Times 1 -Exactly -ParameterFilter {
$ScriptBlock.ToString() -like '*$sync.ItemsControl.IsEnabled = $true*'
}
Should -Invoke -CommandName Invoke-WPFUIThread -Times 1 -Exactly -ParameterFilter {
$ScriptBlock.ToString() -like '*Set-WinUtilTaskbaritem -state "Error" -overlay "warning"*'
}
+25
View File
@@ -69,6 +69,23 @@ Describe "Initialize-WinUtilTabContent" {
$targetGridName -eq "appxpanel" -and $columncount -eq 2
}
}
It "checks for existing Win11ISO work when the tab is initialized" {
Add-Type -AssemblyName WindowsBase
$dispatcher = [pscustomobject]@{}
$dispatcher | Add-Member -MemberType ScriptMethod -Name BeginInvoke -Value {
param($priority, $action)
$action.Invoke()
}
$script:sync.Form = [pscustomobject]@{ Dispatcher = $dispatcher }
Mock Invoke-WinUtilISOCheckExistingWork { }
Initialize-WinUtilTabContent -TabName "Win11ISO"
Initialize-WinUtilTabContent -TabName "Win11ISO"
Should -Invoke -CommandName Invoke-WinUtilISOCheckExistingWork -Times 1 -Exactly
$script:sync.InitializedTabs["Win11ISO"] | Should -BeTrue
}
}
Describe "Startup lazy tab wiring" {
@@ -96,4 +113,12 @@ Describe "Startup lazy tab wiring" {
$rendererScript | Should -Match '\$sync\.Buttons\.Add\(\$button\.Name\)'
$mainScript | Should -Match '\$sync\.Buttons -notcontains \$psitem'
}
It "binds generated documentation links when lazy panels are rendered" {
$rendererScript = Get-Content -Path (Join-Path $script:repoRoot "functions\public\Invoke-WPFUIElements.ps1") -Raw
$mainScript = Get-Content -Path (Join-Path $script:repoRoot "scripts\main.ps1") -Raw
$rendererScript | Should -Match '(?s)if \(\$entryInfo\.Link\).*\$textBlock\.Add_MouseUp\(\{.*Start-Process \$Sender\.ToolTip -ErrorAction Stop'
$mainScript | Should -Not -Match '\.Name\.EndsWith\("Link"\)'
}
}
+139
View File
@@ -0,0 +1,139 @@
#===========================================================================
# Tests - O&O ShutUp10++ Download Workflow
#===========================================================================
BeforeAll {
$script:repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
. (Join-Path $script:repoRoot "functions\private\Save-WinUtilFile.ps1")
. (Join-Path $script:repoRoot "functions\public\Invoke-WPFOOSU.ps1")
function Invoke-WPFRunspace {
param($ArgumentList, $ParameterList, [scriptblock]$ScriptBlock)
}
function Set-WinUtilTweaksProgressIndicator {
param($Visible, $Label, $Percent)
}
function Show-WinUtilMessage {
param($Message, $Title, $Button, $Icon)
}
function Write-WinUtilLog {
param($Message, $Level, $Component)
}
function script:New-WinUtilOOSUTestContext {
param([bool]$ProcessRunning = $false)
$script:sync = [Hashtable]::Synchronized(@{
ProcessRunning = $ProcessRunning
winutildir = $TestDrive
Form = [pscustomobject]@{
Dispatcher = [pscustomobject]@{}
}
})
}
}
Describe "Save-WinUtilFile" {
It "copies a download and reports its percentage" {
$sourcePath = Join-Path $TestDrive "source.bin"
$destinationPath = Join-Path $TestDrive "destination.bin"
$sourceBytes = [byte[]](0..255)
[System.IO.File]::WriteAllBytes($sourcePath, $sourceBytes)
$reportedProgress = [System.Collections.Generic.List[int]]::new()
Save-WinUtilFile -Uri ([uri]$sourcePath) -DestinationPath $destinationPath -ProgressCallback {
param($percent)
$reportedProgress.Add($percent)
}
[System.IO.File]::ReadAllBytes($destinationPath) | Should -Be $sourceBytes
$reportedProgress[-1] | Should -Be 100
}
}
Describe "Invoke-WPFOOSU" {
BeforeEach {
New-WinUtilOOSUTestContext
$script:capturedScriptBlock = $null
$script:capturedParameterList = $null
Mock Invoke-WPFRunspace {
$script:capturedScriptBlock = $ScriptBlock
$script:capturedParameterList = $ParameterList
[pscustomobject]@{ MockHandle = $true }
}
Mock Set-WinUtilTweaksProgressIndicator { }
Mock Show-WinUtilMessage { }
Mock Write-WinUtilLog { }
Mock Start-Process { }
Mock Write-Error { }
}
AfterEach {
Remove-Variable -Name sync -Scope Script -ErrorAction SilentlyContinue
Remove-Variable -Name capturedScriptBlock -Scope Script -ErrorAction SilentlyContinue
Remove-Variable -Name capturedParameterList -Scope Script -ErrorAction SilentlyContinue
}
It "queues the download in a background runspace" {
Invoke-WPFOOSU
$script:sync.ProcessRunning | Should -BeTrue
Should -Invoke Invoke-WPFRunspace -Times 1 -Exactly
$script:capturedParameterList[0][0] | Should -Be "downloadPath"
$script:capturedParameterList[0][1] | Should -Be (Join-Path $TestDrive "ooshutup10.exe")
}
It "does not start while another process is running" {
New-WinUtilOOSUTestContext -ProcessRunning $true
Invoke-WPFOOSU
Should -Invoke Show-WinUtilMessage -Times 1 -Exactly -ParameterFilter {
$Message -eq "Another process is currently running." -and
$Title -eq "WinUtil" -and
$Button -eq "OK" -and
$Icon -eq "Warning"
}
Should -Not -Invoke Invoke-WPFRunspace
}
It "maps download progress to the window indicator and launches O&O ShutUp10++" {
Mock Save-WinUtilFile {
& $ProgressCallback 35
& $ProgressCallback 100
}
Invoke-WPFOOSU
& $script:capturedScriptBlock -downloadPath $script:capturedParameterList[0][1]
Should -Invoke Set-WinUtilTweaksProgressIndicator -Times 1 -Exactly -ParameterFilter {
$Visible -eq $true -and $Label -eq "Downloading O&O ShutUp10++ (0%)" -and $Percent -eq 0
}
Should -Invoke Set-WinUtilTweaksProgressIndicator -Times 1 -Exactly -ParameterFilter {
$Visible -eq $true -and $Label -eq "Downloading O&O ShutUp10++ (35%)" -and $Percent -eq 35
}
Should -Invoke Set-WinUtilTweaksProgressIndicator -Times 1 -Exactly -ParameterFilter {
$Visible -eq $true -and $Label -eq "O&O ShutUp10++ launched" -and $Percent -eq 100
}
Should -Invoke Start-Process -Times 1 -Exactly -ParameterFilter {
$FilePath -eq (Join-Path $TestDrive "ooshutup10.exe")
}
$script:sync.ProcessRunning | Should -BeFalse
}
It "shows failure progress and clears the running state when the download fails" {
Mock Save-WinUtilFile { throw "download failed" }
Invoke-WPFOOSU
& $script:capturedScriptBlock -downloadPath $script:capturedParameterList[0][1]
Should -Invoke Set-WinUtilTweaksProgressIndicator -Times 1 -Exactly -ParameterFilter {
$Visible -eq $true -and $Label -eq "O&O ShutUp10++ download failed" -and $Percent -eq 100
}
Should -Not -Invoke Start-Process
Should -Invoke Write-Error -Times 1 -Exactly
$script:sync.ProcessRunning | Should -BeFalse
}
}
+4
View File
@@ -140,6 +140,10 @@ Describe "Invoke-WPFRunspace behavior" {
$runspaceScript | Should -Not -Match '\$script:powershell'
$runspaceScript | Should -Not -Match '\$script:handle'
}
It "exposes a strongly typed cleanup callback" {
([WinUtilRunspaceCleanup]::Callback -is [System.Threading.WaitOrTimerCallback]) | Should -BeTrue
}
}
Describe "Public runspace callers" {
+22
View File
@@ -172,18 +172,27 @@ namespace Windows.Controls
WPFInstallBrowser = [pscustomobject]@{
Content = "Firefox"
Description = "Fast private browser"
Category = "Browsers"
}
WPFInstallMedia = [pscustomobject]@{
Content = "VLC"
Description = "Media player"
Category = "Multimedia Tools"
}
WPFInstallLiteral = [pscustomobject]@{
Content = "Tool [abc]"
Description = "Literal wildcard sample"
Category = "Utilities"
}
WPFInstallEditor = [pscustomobject]@{
Content = "Code Editor"
Description = "Text editing"
Category = "Development"
}
WPFInstallPowerToys = [pscustomobject]@{
Content = "PowerToys"
Description = "A collection of system utilities"
Category = "Microsoft Tools"
}
}
}
@@ -363,6 +372,19 @@ Describe "Find-AppsByNameOrDescription" {
$mediaItem.Visibility | Should -Be ([Windows.Visibility]::Collapsed)
$category.Visibility | Should -Be ([Windows.Visibility]::Visible)
}
It "filters category chips by exact application category" {
$utilityItem = New-WinUtilAppSearchItem -Tag "WPFInstallLiteral"
$powerToysItem = New-WinUtilAppSearchItem -Tag "WPFInstallPowerToys"
$category = New-WinUtilAppCategory -Label "- Tools" -Items @($utilityItem, $powerToysItem)
New-WinUtilAppSearchContext -Categories @($category)
Find-AppsByNameOrDescription -SearchString "Utilities" -Category "Utilities"
$utilityItem.Visibility | Should -Be ([Windows.Visibility]::Visible)
$powerToysItem.Visibility | Should -Be ([Windows.Visibility]::Collapsed)
$category.Visibility | Should -Be ([Windows.Visibility]::Visible)
}
}
Describe "Find-TweaksByNameOrDescription" {
+70
View File
@@ -6,12 +6,82 @@ $repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
BeforeAll {
$script:repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
. (Join-Path $script:repoRoot "functions\private\Invoke-WinUtilCurrentSystem.ps1")
. (Join-Path $script:repoRoot "functions\private\Set-WinUtilRegistry.ps1")
. (Join-Path $script:repoRoot "functions\private\Set-WinUtilService.ps1")
function winget {
param([Parameter(ValueFromRemainingArguments = $true)]$Arguments)
}
function choco {
param([Parameter(ValueFromRemainingArguments = $true)]$Arguments)
}
function Write-WinUtilLog { }
}
Describe "Invoke-WinUtilCurrentSystem installed apps" {
BeforeEach {
$script:sync = [Hashtable]::Synchronized(@{
configs = [pscustomobject]@{
applicationsHashtable = @{
WPFInstallGit = [pscustomobject]@{ winget = "Git.Git"; choco = "git" }
WPFInstallChatGPT = [pscustomobject]@{ winget = "msstore:9NT1R1C2HH7J"; choco = "na" }
WPFInstallMissing = [pscustomobject]@{ winget = "Git"; choco = "missing" }
}
}
})
Mock winget {
$global:LASTEXITCODE = 0
$script:wingetArguments = @($Arguments)
@(
"Name Id Version Source",
"--------------------------------",
"Git Git.Git 2.0 winget",
"ChatGPT 9NT1R1C2HH7J 1.0 msstore"
)
}
Mock choco {
$script:chocoArguments = @($Arguments)
@("Chocolatey v2", "git 2.0", "2 packages installed.")
}
}
AfterEach {
Remove-Variable -Name sync -Scope Script -ErrorAction SilentlyContinue
Remove-Variable -Name wingetArguments -Scope Script -ErrorAction SilentlyContinue
Remove-Variable -Name chocoArguments -Scope Script -ErrorAction SilentlyContinue
}
It "matches single standard and Microsoft Store package IDs" {
$result = @(Invoke-WinUtilCurrentSystem -CheckBox "winget")
$result | Should -HaveCount 2
$result | Should -Contain "WPFInstallGit"
$result | Should -Contain "WPFInstallChatGPT"
$result | Should -Not -Contain "WPFInstallMissing"
Should -Invoke -CommandName winget -Times 1 -Exactly
$script:wingetArguments | Should -Be @("list", "--accept-source-agreements", "--disable-interactivity")
}
It "fails promptly when Winget cannot list applications" {
Mock winget {
$global:LASTEXITCODE = 1
"winget failed"
}
{ Invoke-WinUtilCurrentSystem -CheckBox "winget" } | Should -Throw "winget list failed with exit code 1."
}
It "matches the primary Chocolatey package ID in one list call" {
$result = @(Invoke-WinUtilCurrentSystem -CheckBox "choco")
$result | Should -Be @("WPFInstallGit")
Should -Invoke -CommandName choco -Times 1 -Exactly
$script:chocoArguments | Should -Be @("list")
}
}
Describe "Set-WinUtilRegistry" {
BeforeEach {
$script:testPathResults = @{}
+10 -3
View File
@@ -19,6 +19,9 @@ BeforeAll {
function Remove-WinUtilAPPX {
param($Name)
}
function Remove-WinUtilProvisionedAPPX {
param($PackageList)
}
function Set-WinUtilDNS {
param($DNSProvider)
}
@@ -28,8 +31,8 @@ BeforeAll {
function Invoke-WPFUIThread {
param([scriptblock]$ScriptBlock)
}
function Set-WinUtilProgressBar {
param($Label, $Percent)
function Set-WinUtilTweaksProgressIndicator {
param($Visible, $Label, $Percent)
}
function Write-WinUtilLog {
param($Message, $Level, $Component)
@@ -89,6 +92,7 @@ Describe "Invoke-WinUtilTweaks" {
Mock Set-WinUtilRegistry { }
Mock Invoke-WinUtilScript { }
Mock Remove-WinUtilAPPX { }
Mock Remove-WinUtilProvisionedAPPX { }
Mock Write-WinUtilLog { }
Mock Write-Warning { }
}
@@ -118,6 +122,9 @@ Describe "Invoke-WinUtilTweaks" {
Should -Invoke -CommandName Remove-WinUtilAPPX -Times 1 -Exactly -ParameterFilter {
$Name -eq "Microsoft.ExampleApp"
}
Should -Invoke -CommandName Remove-WinUtilProvisionedAPPX -Times 1 -Exactly -ParameterFilter {
$PackageList.Count -eq 1 -and $PackageList[0] -eq "Microsoft.ExampleApp"
}
}
It "uses original registry values and service startup types in undo mode" {
@@ -137,6 +144,7 @@ Describe "Invoke-WinUtilTweaks" {
$Name -eq "WPFTweaksExample" -and $ScriptBlock.ToString() -eq "Write-Output 'undo tweak'"
}
Should -Invoke -CommandName Remove-WinUtilAPPX -Times 0 -Exactly
Should -Invoke -CommandName Remove-WinUtilProvisionedAPPX -Times 0 -Exactly
}
It "keeps a user-changed service startup type by default" {
@@ -176,7 +184,6 @@ Describe "Invoke-WPFtweaksbutton" {
Mock Invoke-WPFRunspace { [pscustomobject]@{ MockHandle = $true } }
Mock Invoke-WinUtilTweaks { }
Mock Invoke-WPFUIThread { }
Mock Set-WinUtilProgressBar { }
Mock Write-WinUtilLog { }
Mock Write-Host { }
}
+202 -1
View File
@@ -5,7 +5,7 @@
BeforeAll {
$script:repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
if (-not ("System.Windows.Controls.CheckBox" -as [type])) {
if (-not ("Windows.Visibility" -as [type])) {
Add-Type @"
namespace Windows
{
@@ -15,7 +15,25 @@ namespace Windows
Collapsed
}
}
"@
}
if (-not ("Windows.WindowState" -as [type])) {
Add-Type @"
namespace Windows
{
public enum WindowState
{
Normal,
Minimized,
Maximized
}
}
"@
}
if (-not ("System.Windows.Controls.CheckBox" -as [type])) {
Add-Type @"
namespace System.Windows.Controls
{
public class CheckBox
@@ -43,9 +61,33 @@ namespace System.Windows.Controls
. (Join-Path $script:repoRoot "functions\private\Update-WinUtilSelections.ps1")
. (Join-Path $script:repoRoot "functions\private\Reset-WPFCheckBoxes.ps1")
. (Join-Path $script:repoRoot "functions\public\Invoke-WPFGetInstalled.ps1")
. (Join-Path $script:repoRoot "functions\public\Invoke-WPFSelectedCheckboxesUpdate.ps1")
. (Join-Path $script:repoRoot "functions\public\Invoke-WPFButton.ps1")
. (Join-Path $script:repoRoot "functions\public\Invoke-WPFToggleAllCategories.ps1")
function Set-WinUtilTweaksProgressIndicator {
param($Visible, $Label, $Percent)
}
function Invoke-WPFRunspace {
param($ArgumentList, $ParameterList, [scriptblock]$ScriptBlock)
}
function Invoke-WPFUIThread {
param([scriptblock]$ScriptBlock)
}
function Invoke-WinUtilCurrentSystem {
param($CheckBox)
}
function Set-WinUtilTaskbaritem {
param($state)
}
function Test-WinUtilPackageManager {
param([switch]$winget)
}
function Write-WinUtilLog {
param($Message, $Level, $Component)
}
function script:New-WinUtilFakeCheckBox {
param([bool]$IsChecked = $false)
@@ -157,6 +199,9 @@ Describe "Invoke-WPFSelectedCheckboxesUpdate" {
@($script:sync.selectedToggles) | Should -Be @("WPFToggleDarkMode")
@($script:sync.selectedFeatures) | Should -Be @("WPFFeatureSandbox")
@($script:sync.selectedAppx) | Should -Be @("WPFAppxExample")
$script:sync.WPFselectedAppsButton.Content | Should -Be "Selected Apps: 1"
$script:sync.selectedAppsstackPanel.Children.Count | Should -Be 1
$script:sync.selectedAppsstackPanel.Children[0].Key | Should -Be "WPFInstallGit"
}
It "removes checkbox keys from the matching selected lists" {
@@ -177,6 +222,94 @@ Describe "Invoke-WPFSelectedCheckboxesUpdate" {
$script:sync.selectedToggles.Count | Should -Be 0
$script:sync.selectedFeatures.Count | Should -Be 0
$script:sync.selectedAppx.Count | Should -Be 0
$script:sync.WPFselectedAppsButton.Content | Should -Be "Selected Apps: 0"
$script:sync.selectedAppsstackPanel.Children.Count | Should -Be 0
}
}
Describe "Invoke-WPFGetInstalled selection state" {
BeforeEach {
New-WinUtilUiStateTestContext
$script:sync.ProcessRunning = $false
$script:sync.ChocoRadioButton = [pscustomobject]@{ IsChecked = $false }
$script:sync.preferences = [pscustomobject]@{ packagemanager = "Winget" }
$script:sync.WPFInstallGit = New-WinUtilFakeCheckBox
$dispatcher = [pscustomobject]@{}
$dispatcher | Add-Member -MemberType ScriptMethod -Name BeginInvoke -Value {
param($Action, [object[]]$Arguments)
$Action.DynamicInvoke($Arguments)
}
$script:sync.Form = [pscustomobject]@{ Dispatcher = $dispatcher }
$script:capturedGetInstalledScriptBlock = $null
$script:capturedGetInstalledParameters = @{}
Mock Test-WinUtilPackageManager { "installed" }
Mock Invoke-WinUtilCurrentSystem { @("WPFInstallGit") }
Mock Set-WinUtilTaskbaritem { }
Mock Write-WinUtilLog { }
Mock Write-Warning { }
Mock Invoke-WPFRunspace {
$script:capturedGetInstalledScriptBlock = $ScriptBlock
foreach ($parameter in $ParameterList) {
$script:capturedGetInstalledParameters[$parameter[0]] = $parameter[1]
}
}
}
AfterEach {
Remove-Variable -Name sync -Scope Script -ErrorAction SilentlyContinue
Remove-Variable -Name sync -Scope Global -ErrorAction SilentlyContinue
Remove-Variable -Name capturedGetInstalledScriptBlock -Scope Script -ErrorAction SilentlyContinue
Remove-Variable -Name capturedGetInstalledParameters -Scope Script -ErrorAction SilentlyContinue
}
It "updates the selected app model, checkbox, count, and popup" {
Invoke-WPFGetInstalled -CheckBox "winget"
& $script:capturedGetInstalledScriptBlock `
-checkbox "winget" `
-managerPreference "Winget" `
-operation $script:capturedGetInstalledParameters.operation `
-completeAction $script:capturedGetInstalledParameters.completeAction
@($script:sync.selectedApps) | Should -Be @("WPFInstallGit")
$script:sync.WPFInstallGit.IsChecked | Should -BeTrue
$script:sync.WPFselectedAppsButton.Content | Should -Be "Selected Apps: 1"
$script:sync.selectedAppsstackPanel.Children.Count | Should -Be 1
$script:sync.selectedAppsstackPanel.Children[0].Key | Should -Be "WPFInstallGit"
}
It "clears the running state when detection fails" {
Mock Invoke-WinUtilCurrentSystem { throw "detection failed" }
Invoke-WPFGetInstalled -CheckBox "winget"
& $script:capturedGetInstalledScriptBlock `
-checkbox "winget" `
-managerPreference "Winget" `
-operation $script:capturedGetInstalledParameters.operation `
-completeAction $script:capturedGetInstalledParameters.completeAction
$script:sync.ProcessRunning | Should -BeFalse
Should -Invoke -CommandName Write-WinUtilLog -Times 1 -Exactly -ParameterFilter {
$Level -eq "ERROR" -and
$Component -eq "Install" -and
$Message -eq "Get installed state failed: detection failed"
}
Should -Invoke -CommandName Set-WinUtilTaskbaritem -Times 1 -Exactly -ParameterFilter { $state -eq "None" }
}
It "clears the running state when the worker cannot be queued" {
Mock Invoke-WPFRunspace { throw "queue failed" }
Invoke-WPFGetInstalled -CheckBox "winget"
$script:sync.ProcessRunning | Should -BeFalse
Should -Invoke -CommandName Write-WinUtilLog -Times 1 -Exactly -ParameterFilter {
$Level -eq "ERROR" -and
$Component -eq "Install" -and
$Message -eq "Get installed state failed: queue failed"
}
Should -Invoke -CommandName Set-WinUtilTaskbaritem -Times 1 -Exactly -ParameterFilter { $state -eq "None" }
}
}
@@ -272,3 +405,71 @@ Describe "Invoke-WPFToggleAllCategories" {
}
}
}
Describe "Invoke-WPFButton progress cleanup" {
BeforeEach {
New-WinUtilUiStateTestContext
Mock Set-WinUtilTweaksProgressIndicator { }
}
AfterEach {
Remove-Variable -Name sync -Scope Script -ErrorAction SilentlyContinue
Remove-Variable -Name sync -Scope Global -ErrorAction SilentlyContinue
}
It "clears completed progress on the next idle button click" {
$script:sync.ProcessRunning = $false
Invoke-WPFButton -Button "WPFNoOp"
Should -Invoke Set-WinUtilTweaksProgressIndicator -Times 1 -Exactly -ParameterFilter {
$Visible -eq $false
}
}
It "leaves progress visible while a process is running" {
$script:sync.ProcessRunning = $true
Invoke-WPFButton -Button "WPFNoOp"
Should -Not -Invoke Set-WinUtilTweaksProgressIndicator
}
It "leaves progress visible while a Win11 ISO process is running" {
$script:sync.ProcessRunning = $false
$script:sync.Win11ISOProcessRunning = $true
Invoke-WPFButton -Button "WPFNoOp"
Should -Not -Invoke Set-WinUtilTweaksProgressIndicator
}
}
Describe "Invoke-WPFButton window state" {
BeforeEach {
New-WinUtilUiStateTestContext
$script:sync.ProcessRunning = $true
$script:sync.Form = [pscustomobject]@{
WindowState = [Windows.WindowState]::Normal
}
}
AfterEach {
Remove-Variable -Name sync -Scope Script -ErrorAction SilentlyContinue
Remove-Variable -Name sync -Scope Global -ErrorAction SilentlyContinue
}
It "maximizes a normal window" {
Invoke-WPFButton -Button "WPFMaximizeButton"
$script:sync.Form.WindowState | Should -Be ([Windows.WindowState]::Maximized)
}
It "restores a maximized window" {
$script:sync.Form.WindowState = [Windows.WindowState]::Maximized
Invoke-WPFButton -Button "WPFMaximizeButton"
$script:sync.Form.WindowState | Should -Be ([Windows.WindowState]::Normal)
}
}
+136 -36
View File
@@ -11,8 +11,11 @@ BeforeAll {
function Write-WinUtilLog {
param($Message, $Level, $Component)
}
function Show-WinUtilMessage {
param($Message, $Title, $Button, $Icon)
}
function Get-ScheduledTask {
param($TaskPath)
param($TaskPath, $ErrorAction)
}
function Disable-ScheduledTask {
param(
@@ -51,6 +54,7 @@ Describe "Invoke-WPFUpdatesdisable" {
BeforeEach {
Mock Write-Host { }
Mock Write-WinUtilLog { }
Mock Show-WinUtilMessage { "Yes" }
Mock New-Item { }
Mock Set-ItemProperty { }
Mock Set-Service { }
@@ -88,25 +92,23 @@ Describe "Invoke-WPFUpdatesdisable" {
$Type -eq "DWord" -and
$Value -eq 0
}
Should -Invoke -CommandName Set-ItemProperty -Times 1 -Exactly -ParameterFilter {
$Path -eq "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer" -and
$Name -eq "SettingsPageVisibility" -and
$Value -eq "hide:windowsupdate"
}
}
It "disables update services and clears the SoftwareDistribution folder" {
Invoke-WPFUpdatesdisable
foreach ($expectedServiceName in @("BITS", "wuauserv", "UsoSvc")) {
$expected = $expectedServiceName
Should -Invoke -CommandName Stop-Service -Times 1 -Exactly -ParameterFilter {
$Name -eq $expected -and $Force -eq $true
}
}
Should -Invoke -CommandName Set-Service -Times 1 -Exactly -ParameterFilter {
$Name -eq "BITS" -and $StartupType -eq "Disabled"
}
Should -Invoke -CommandName Set-Service -Times 1 -Exactly -ParameterFilter {
$Name -eq "wuauserv" -and $StartupType -eq "Disabled"
}
Should -Invoke -CommandName Stop-Service -Times 1 -Exactly -ParameterFilter {
$Name -eq "UsoSvc" -and $Force -eq $true
}
Should -Invoke -CommandName Set-Service -Times 1 -Exactly -ParameterFilter {
$Name -eq "UsoSvc" -and $StartupType -eq "Disabled"
}
@@ -128,6 +130,21 @@ Describe "Invoke-WPFUpdatesdisable" {
}
Should -Invoke -CommandName Disable-ScheduledTask -Times $script:updateTaskPaths.Count -Exactly
}
It "requires confirmation before disabling updates" {
Mock Show-WinUtilMessage { "No" }
Invoke-WPFUpdatesdisable
Should -Invoke Show-WinUtilMessage -Times 1 -Exactly -ParameterFilter {
$Title -eq "Disable Windows Update?" -and
$Button -eq "YesNo" -and
$Icon -eq "Warning"
}
Should -Not -Invoke Set-ItemProperty
Should -Not -Invoke Set-Service
Should -Not -Invoke Remove-Item
}
}
Describe "Invoke-WPFUpdatesdefault" {
@@ -136,6 +153,11 @@ Describe "Invoke-WPFUpdatesdefault" {
Mock Write-WinUtilLog { }
Mock Remove-Item { }
Mock Remove-ItemProperty { }
Mock Get-ItemProperty {
[pscustomobject]@{
SettingsPageVisibility = "hide:windowsupdate"
}
}
Mock Set-Service { }
Mock Start-Service { }
Mock Get-ScheduledTask {
@@ -147,30 +169,49 @@ Describe "Invoke-WPFUpdatesdefault" {
Mock secedit { }
}
It "removes update policy registry paths and shows the Windows Update settings page" {
It "removes only registry values managed by WinUtil" {
Invoke-WPFUpdatesdefault
$expectedPaths = @(
"HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU",
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\DeliveryOptimization",
"HKLM:\SOFTWARE\Microsoft\WindowsUpdate\UX\Settings",
"HKLM:\SOFTWARE\Policies\Microsoft\Windows\Device Metadata",
"HKLM:\SOFTWARE\Policies\Microsoft\Windows\DriverSearching",
"HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate"
$expectedRegistryValues = @(
@("HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU", "NoAutoUpdate", "AUOptions", "NoAutoRebootWithLoggedOnUsers", "AUPowerManagement"),
@("HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate", "ExcludeWUDriversInQualityUpdate", "DeferFeatureUpdates", "DeferFeatureUpdatesPeriodInDays", "DeferQualityUpdates", "DeferQualityUpdatesPeriodInDays"),
@("HKLM:\SOFTWARE\Microsoft\WindowsUpdate\UX\Settings", "BranchReadinessLevel", "DeferFeatureUpdatesPeriodInDays", "DeferQualityUpdatesPeriodInDays"),
@("HKLM:\SOFTWARE\Policies\Microsoft\Windows\Device Metadata", "PreventDeviceMetadataFromNetwork"),
@("HKLM:\SOFTWARE\Policies\Microsoft\Windows\DriverSearching", "DontPromptForWindowsUpdate", "DontSearchWindowsUpdate", "DriverUpdateWizardWuSearchEnabled"),
@("HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\DeliveryOptimization\Config", "DODownloadMode")
)
foreach ($expectedRegistryPath in $expectedPaths) {
$expected = $expectedRegistryPath
Should -Invoke -CommandName Remove-Item -Times 1 -Exactly -ParameterFilter {
$Path -eq $expected -and $Recurse -eq $true -and $Force -eq $true
foreach ($expectedEntry in $expectedRegistryValues) {
$expectedPath = $expectedEntry[0]
foreach ($expectedName in $expectedEntry[1..($expectedEntry.Count - 1)]) {
$valueName = $expectedName
Should -Invoke -CommandName Remove-ItemProperty -Times 1 -Exactly -ParameterFilter {
$Path -eq $expectedPath -and $Name -eq $valueName
}
}
}
Should -Not -Invoke Remove-Item
Should -Invoke -CommandName Remove-ItemProperty -Times 1 -Exactly -ParameterFilter {
$Path -eq "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer" -and
$Name -eq "SettingsPageVisibility"
}
}
It "preserves unrelated Settings page visibility policy" {
Mock Get-ItemProperty {
[pscustomobject]@{
SettingsPageVisibility = "hide:privacy"
}
}
Invoke-WPFUpdatesdefault
Should -Not -Invoke Remove-ItemProperty -ParameterFilter {
$Path -eq "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer" -and
$Name -eq "SettingsPageVisibility"
}
}
It "restores update service startup types" {
Invoke-WPFUpdatesdefault
@@ -186,12 +227,12 @@ Describe "Invoke-WPFUpdatesdefault" {
Should -Invoke -CommandName Set-Service -Times 1 -Exactly -ParameterFilter {
$Name -eq "UsoSvc" -and $StartupType -eq "Automatic"
}
Should -Invoke -CommandName Set-Service -Times 1 -Exactly -ParameterFilter {
$Name -eq "WaaSMedicSvc" -and $StartupType -eq "Manual"
Should -Not -Invoke Set-Service -ParameterFilter {
$Name -eq "WaaSMedicSvc"
}
}
It "enables update scheduled task paths and resets local policy defaults" {
It "enables update scheduled task paths without resetting unrelated local security policy" {
Invoke-WPFUpdatesdefault
foreach ($expectedTaskPath in $script:updateTaskPaths) {
@@ -201,13 +242,7 @@ Describe "Invoke-WPFUpdatesdefault" {
}
}
Should -Invoke -CommandName Enable-ScheduledTask -Times $script:updateTaskPaths.Count -Exactly
Should -Invoke -CommandName secedit -Times 1 -Exactly -ParameterFilter {
$Arguments[0] -eq "/configure" -and
$Arguments[1] -eq "/cfg" -and
$Arguments[2] -like "*\inf\defltbase.inf" -and
$Arguments[3] -eq "/db" -and
$Arguments[4] -eq "defltbase.sdb"
}
Should -Not -Invoke secedit
}
}
@@ -217,6 +252,47 @@ Describe "Invoke-WPFUpdatessecurity" {
Mock Write-WinUtilLog { }
Mock New-Item { }
Mock Set-ItemProperty { }
Mock Remove-ItemProperty { }
Mock Set-Service { }
Mock Start-Service { }
Mock Get-ScheduledTask {
[pscustomobject]@{
TaskPath = $TaskPath
}
}
Mock Enable-ScheduledTask { }
}
It "restores update availability before applying recommended settings" {
Invoke-WPFUpdatessecurity
Should -Invoke -CommandName Remove-ItemProperty -Times 1 -Exactly -ParameterFilter {
$Path -eq "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" -and
$Name -eq "NoAutoUpdate"
}
Should -Invoke -CommandName Remove-ItemProperty -Times 1 -Exactly -ParameterFilter {
$Path -eq "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\DeliveryOptimization\Config" -and
$Name -eq "DODownloadMode"
}
Should -Invoke -CommandName Set-Service -Times 1 -Exactly -ParameterFilter {
$Name -eq "BITS" -and $StartupType -eq "Manual"
}
Should -Invoke -CommandName Set-Service -Times 1 -Exactly -ParameterFilter {
$Name -eq "wuauserv" -and $StartupType -eq "Manual"
}
Should -Invoke -CommandName Set-Service -Times 1 -Exactly -ParameterFilter {
$Name -eq "UsoSvc" -and $StartupType -eq "Automatic"
}
Should -Invoke -CommandName Start-Service -Times 1 -Exactly -ParameterFilter {
$Name -eq "UsoSvc"
}
foreach ($expectedTaskPath in $script:updateTaskPaths) {
$expected = $expectedTaskPath
Should -Invoke -CommandName Get-ScheduledTask -Times 1 -Exactly -ParameterFilter {
$TaskPath -eq $expected
}
}
Should -Invoke -CommandName Enable-ScheduledTask -Times $script:updateTaskPaths.Count -Exactly
}
It "disables driver metadata and Windows Update driver search" {
@@ -261,23 +337,35 @@ Describe "Invoke-WPFUpdatessecurity" {
Invoke-WPFUpdatessecurity
Should -Invoke -CommandName Set-ItemProperty -Times 1 -Exactly -ParameterFilter {
$Path -eq "HKLM:\SOFTWARE\Microsoft\WindowsUpdate\UX\Settings" -and
$Name -eq "BranchReadinessLevel" -and
$Path -eq "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" -and
$Name -eq "DeferFeatureUpdates" -and
$Type -eq "DWord" -and
$Value -eq 20
$Value -eq 1
}
Should -Invoke -CommandName Set-ItemProperty -Times 1 -Exactly -ParameterFilter {
$Path -eq "HKLM:\SOFTWARE\Microsoft\WindowsUpdate\UX\Settings" -and
$Path -eq "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" -and
$Name -eq "DeferFeatureUpdatesPeriodInDays" -and
$Type -eq "DWord" -and
$Value -eq 365
}
Should -Invoke -CommandName Set-ItemProperty -Times 1 -Exactly -ParameterFilter {
$Path -eq "HKLM:\SOFTWARE\Microsoft\WindowsUpdate\UX\Settings" -and
$Path -eq "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" -and
$Name -eq "DeferQualityUpdates" -and
$Type -eq "DWord" -and
$Value -eq 1
}
Should -Invoke -CommandName Set-ItemProperty -Times 1 -Exactly -ParameterFilter {
$Path -eq "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" -and
$Name -eq "DeferQualityUpdatesPeriodInDays" -and
$Type -eq "DWord" -and
$Value -eq 4
}
Should -Invoke -CommandName Set-ItemProperty -Times 1 -Exactly -ParameterFilter {
$Path -eq "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" -and
$Name -eq "AUOptions" -and
$Type -eq "DWord" -and
$Value -eq 4
}
Should -Invoke -CommandName Set-ItemProperty -Times 1 -Exactly -ParameterFilter {
$Path -eq "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" -and
$Name -eq "NoAutoRebootWithLoggedOnUsers" -and
@@ -291,4 +379,16 @@ Describe "Invoke-WPFUpdatessecurity" {
$Value -eq 0
}
}
It "removes legacy WinUtil deferral values from the unsupported UX settings path" {
Invoke-WPFUpdatessecurity
foreach ($expectedValueName in @("BranchReadinessLevel", "DeferFeatureUpdatesPeriodInDays", "DeferQualityUpdatesPeriodInDays")) {
$expected = $expectedValueName
Should -Invoke Remove-ItemProperty -Times 1 -Exactly -ParameterFilter {
$Path -eq "HKLM:\SOFTWARE\Microsoft\WindowsUpdate\UX\Settings" -and
$Name -eq $expected
}
}
}
}
+337 -82
View File
@@ -6,6 +6,7 @@ Describe "Win11 Creator setup media" {
BeforeAll {
$script:repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
$script:isoWorkflowPath = Join-Path $script:repoRoot "functions\private\Invoke-WinUtilISO.ps1"
$script:isoUsbWorkflowPath = Join-Path $script:repoRoot "functions\private\Invoke-WinUtilISOUSB.ps1"
$script:isoScriptPath = Join-Path $script:repoRoot "functions\private\Invoke-WinUtilISOScript.ps1"
$script:autoUnattendPath = Join-Path $script:repoRoot "tools\autounattend.xml"
@@ -36,11 +37,12 @@ Describe "Win11 Creator setup media" {
}
$script:modifyFunction = Get-WinUtilFunctionText -Path $script:isoWorkflowPath -FunctionName "Invoke-WinUtilISOModify"
$script:mountAndVerifyFunction = Get-WinUtilFunctionText -Path $script:isoWorkflowPath -FunctionName "Invoke-WinUtilISOMountAndVerify"
$script:cleanAndResetFunction = Get-WinUtilFunctionText -Path $script:isoWorkflowPath -FunctionName "Invoke-WinUtilISOCleanAndReset"
$script:exportFunction = Get-WinUtilFunctionText -Path $script:isoWorkflowPath -FunctionName "Invoke-WinUtilISOExport"
$script:writeUsbFunction = Get-WinUtilFunctionText -Path $script:isoUsbWorkflowPath -FunctionName "Invoke-WinUtilISOWriteUSB"
$script:editionIdFunction = Get-WinUtilFunctionText -Path $script:isoWorkflowPath -FunctionName "Get-WinUtilEditionIdFromName"
$script:addDriversFunction = Get-WinUtilFunctionText -Path $script:isoScriptPath -FunctionName "Add-DriversToImage"
$script:answerFileChildElementFunction = Get-WinUtilFunctionText -Path $script:isoScriptPath -FunctionName "Get-WinUtilISOScriptChildElement"
$script:answerFileConversionFunction = Get-WinUtilFunctionText -Path $script:isoScriptPath -FunctionName "ConvertTo-WinUtilISOAnswerFile"
$script:editionConfigFunction = Get-WinUtilFunctionText -Path $script:isoScriptPath -FunctionName "Write-WinUtilISOEditionConfig"
$script:wimMetadataAssertionFunction = Get-WinUtilFunctionText -Path $script:isoScriptPath -FunctionName "Assert-WinUtilISOWimMetadata"
}
It "autounattend template does not force a product key" {
@@ -54,12 +56,35 @@ Describe "Win11 Creator setup media" {
}
}
It "ISO script accepts selected edition setup metadata" {
It "sets every hardware bypass before Windows Setup checks requirements" {
[xml]$xml = Get-Content -Path $script:autoUnattendPath -Raw
$nsMgr = New-Object System.Xml.XmlNamespaceManager($xml.NameTable)
$nsMgr.AddNamespace("u", "urn:schemas-microsoft-com:unattend")
$paths = @($xml.SelectNodes('/u:unattend/u:settings[@pass="windowsPE"]/u:component[@name="Microsoft-Windows-Setup"]/u:RunSynchronous/u:RunSynchronousCommand/u:Path', $nsMgr) | ForEach-Object InnerText) -join "`n"
foreach ($bypass in 'BypassTPMCheck', 'BypassSecureBootCheck', 'BypassRAMCheck', 'BypassCPUCheck', 'BypassStorageCheck') {
$paths | Should -Match ([regex]::Escape($bypass))
}
}
It "sets OOBE-sensitive registry values before OOBE starts" {
[xml]$xml = Get-Content -Path $script:autoUnattendPath -Raw
$nsMgr = New-Object System.Xml.XmlNamespaceManager($xml.NameTable)
$nsMgr.AddNamespace("u", "urn:schemas-microsoft-com:unattend")
$paths = @($xml.SelectNodes('/u:unattend/u:settings[@pass="specialize"]/u:component[@name="Microsoft-Windows-Deployment"]/u:RunSynchronous/u:RunSynchronousCommand/u:Path', $nsMgr) | ForEach-Object InnerText) -join "`n"
foreach ($valueName in 'BypassNRO', 'PreventDeviceEncryption', 'ShippedWithReserves') {
$paths | Should -Match ([regex]::Escape($valueName))
}
}
It "ISO script accepts selected edition and driver-only WIM servicing metadata" {
$isoScriptPath = Join-Path $PSScriptRoot "..\functions\private\Invoke-WinUtilISOScript.ps1"
$content = Get-Content -Path $isoScriptPath -Raw
foreach ($pattern in @(
'\[string\]\$InstallEditionId',
'\[string\]\$InstallImagePath',
'\[int\]\$InstallImageIndex',
'sources\\ei\.cfg',
'PID\.txt'
@@ -81,14 +106,89 @@ Describe "Win11 Creator setup media" {
$script:modifyFunction | Should -Not -Match ([regex]::Escape("Reusing existing temp directory"))
}
It "mounts the copied image file that was verified from the ISO" {
It "keeps WIM servicing limited to one driver-only mount and commit" {
$isoScriptContent = Get-Content -Path $script:isoScriptPath -Raw
foreach ($expectedText in @(
'$sourceImageFileName = Split-Path $wimPath -Leaf',
'$localWim = Join-Path $isoContents "sources\$sourceImageFileName"',
'Copied ISO image file not found: sources\$sourceImageFileName'
"'/Mount-Image'",
"'/Add-Driver'",
"'/Commit'",
"`$mountDir = Join-Path (Split-Path -Path `$ContentRoot -Parent) 'wim_mount'",
'install.wim metadata validation passed'
)) {
$script:modifyFunction | Should -Match ([regex]::Escape($expectedText))
$isoScriptContent | Should -Match ([regex]::Escape($expectedText))
}
foreach ($forbiddenText in @(
'Mount-WindowsImage',
'Dismount-WindowsImage',
'Export-WindowsImage',
'Set-WindowsImage',
'/ResetBase',
'/Cleanup-Image'
)) {
$isoScriptContent | Should -Not -Match ([regex]::Escape($forbiddenText))
}
}
It "stages only boot-storage drivers in WinPE" {
$isoScriptContent = Get-Content -Path $script:isoScriptPath -Raw
$isoScriptContent | Should -Match ([regex]::Escape("Join-Path `$ContentRoot '`$WinpeDriver$'"))
$isoScriptContent | Should -Match 'SCSIAdapter\|HDC'
$isoScriptContent | Should -Not -Match ([regex]::Escape('sources\$OEM$\$$\Drivers'))
$isoScriptContent | Should -Not -Match ([regex]::Escape('WinUtil-InstallDrivers.ps1'))
$isoScriptContent | Should -Not -Match ([regex]::Escape('SetupComplete.cmd'))
}
It "rejects invalid WIM metadata before and after driver injection" {
. ([scriptblock]::Create($script:wimMetadataAssertionFunction))
$valid = @{ Languages = 'en-US'; Installation = 'Client'; Edition = 'Professional'; ProductSuite = 'Terminal Server'; ProductType = 'WinNT' }
$invalidBefore = $valid.Clone()
$invalidBefore.Edition = '<undefined>'
$invalidAfter = $valid.Clone()
$invalidAfter.ProductType = '<undefined>'
{ Assert-WinUtilISOWimMetadata -Before $invalidBefore } | Should -Throw '*already invalid*'
{ Assert-WinUtilISOWimMetadata -Before $valid -After $invalidAfter } | Should -Throw '*validation failed*'
}
It "tracks every background ISO workflow with the shared busy state" {
foreach ($functionText in @(
$script:mountAndVerifyFunction,
$script:modifyFunction,
$script:cleanAndResetFunction,
$script:exportFunction,
$script:writeUsbFunction
)) {
$functionText | Should -Match ([regex]::Escape('$sync["Win11ISOProcessRunning"] = $true'))
$functionText | Should -Match ([regex]::Escape('$sync["Win11ISOProcessRunning"] = $false'))
}
}
It "runs ISO mount and verification outside the UI thread" {
$script:mountAndVerifyFunction | Should -Match ([regex]::Escape("Invoke-WPFRunspace -ParameterList @(,('isoPath', `$isoPath))"))
$script:mountAndVerifyFunction | Should -Match ([regex]::Escape('Invoke-WPFUIThread {'))
$script:mountAndVerifyFunction | Should -Match ([regex]::Escape('Write-WinUtilISOLog'))
$script:mountAndVerifyFunction | Should -Not -Match ([regex]::Escape('Write-Win11ISOLog'))
$script:mountAndVerifyFunction | Should -Match ([regex]::Escape('$sync["WPFWin11ISOBrowseButton"].IsEnabled = $false'))
$script:mountAndVerifyFunction | Should -Match ([regex]::Escape('$sync["WPFWin11ISOBrowseButton"].IsEnabled = $true'))
$script:mountAndVerifyFunction | Should -Match ([regex]::Escape('$sync["WPFWin11ISOMountButton"].IsEnabled = $false'))
$script:mountAndVerifyFunction | Should -Match ([regex]::Escape('$sync["WPFWin11ISOMountButton"].IsEnabled = $true'))
$script:mountAndVerifyFunction | Should -Match ([regex]::Escape('$sync["WPFWin11ISOModifyButton"].IsEnabled = $false'))
$script:mountAndVerifyFunction | Should -Match ([regex]::Escape('$sync["WPFWin11ISOModifyButton"].IsEnabled = $true'))
}
It "blocks oversized install.esd before USB erase confirmation" {
$script:writeUsbFunction | Should -Match ([regex]::Escape('$installEsd = Join-Path $contentsDir "sources\install.esd"'))
$script:writeUsbFunction | Should -Match ([regex]::Escape('$esdSizeBytes -ge 4GB'))
$script:writeUsbFunction | Should -Match 'install\.esd file'
$guardIndex = $script:writeUsbFunction.IndexOf('$installEsd = Join-Path $contentsDir "sources\install.esd"')
$confirmationIndex = $script:writeUsbFunction.IndexOf('Confirm USB Erase')
$guardIndex | Should -BeGreaterThan -1
$confirmationIndex | Should -BeGreaterThan $guardIndex
}
It "maps Windows edition names to setup edition IDs" {
@@ -119,9 +219,7 @@ Describe "Win11 Creator setup media" {
Get-WinUtilEditionIdFromName -EditionName "Windows 11 Unknown Edition" | Should -Be ""
}
It "writes ei.cfg and removes stale PID.txt for selected editions" {
. ([scriptblock]::Create($script:editionConfigFunction))
It "writes ei.cfg and removes stale PID.txt for the selected edition" {
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoConfig_$([guid]::NewGuid())"
$sourcesDir = Join-Path $contentRoot "sources"
$logs = [System.Collections.Generic.List[string]]::new()
@@ -130,8 +228,10 @@ Describe "Win11 Creator setup media" {
try {
New-Item -Path $sourcesDir -ItemType Directory -Force | Out-Null
Set-Content -Path (Join-Path $sourcesDir "PID.txt") -Value "stale-key" -Encoding UTF8
Set-Content -Path (Join-Path $sourcesDir "ei.cfg") -Value "stale-cfg" -Encoding UTF8
Write-WinUtilISOEditionConfig -ContentRoot $contentRoot -EditionId "Professional" -Logger $logger
. $script:isoScriptPath
Invoke-WinUtilISOScript -ISOContentsDir $contentRoot -AutoUnattendXml (Get-Content -Path $script:autoUnattendPath -Raw) -InstallEditionId "Professional" -Log $logger
Test-Path (Join-Path $sourcesDir "PID.txt") | Should -BeFalse
Test-Path (Join-Path $sourcesDir "ei.cfg") | Should -BeTrue
@@ -144,90 +244,245 @@ Describe "Win11 Creator setup media" {
}
}
It "skips ei.cfg when selected edition ID is unknown" {
. ([scriptblock]::Create($script:editionConfigFunction))
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoConfig_$([guid]::NewGuid())"
$logs = [System.Collections.Generic.List[string]]::new()
$logger = { param($message) $logs.Add([string]$message) }
It "stages the complete WinUtil customization script and selected image index" {
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoAnswerFile_$([guid]::NewGuid())"
$template = Get-Content -Path $script:autoUnattendPath -Raw
try {
New-Item -Path $contentRoot -ItemType Directory -Force | Out-Null
. $script:isoScriptPath
Invoke-WinUtilISOScript -ISOContentsDir $contentRoot -AutoUnattendXml $template -InstallEditionId "Core" -InstallImageIndex 6
Write-WinUtilISOEditionConfig -ContentRoot $contentRoot -EditionId "" -Logger $logger
[xml]$answerFile = Get-Content -Path (Join-Path $contentRoot "autounattend.xml") -Raw
$nsMgr = New-Object System.Xml.XmlNamespaceManager($answerFile.NameTable)
$nsMgr.AddNamespace("u", "urn:schemas-microsoft-com:unattend")
$nsMgr.AddNamespace("sg", "https://schneegans.de/windows/unattend-generator/")
Test-Path (Join-Path $contentRoot "sources\ei.cfg") | Should -BeFalse
($logs -join "|") | Should -Match "selected edition ID is unknown"
$answerFile.SelectSingleNode('/u:unattend/u:settings[@pass="windowsPE"]/u:component[@name="Microsoft-Windows-Setup"]/u:ImageInstall/u:OSImage/u:InstallFrom/u:MetaData[u:Key="/IMAGE/INDEX"]/u:Value', $nsMgr).InnerText | Should -Be '6'
$postInstallFile = $answerFile.SelectSingleNode('//sg:File[@path="C:\Windows\Setup\Scripts\WinUtil-PostInstall.ps1"]', $nsMgr)
$postInstallFile | Should -Not -BeNullOrEmpty
$postInstallFile.InnerText | Should -Match 'Remove-AppxProvisionedPackage'
$postInstallFile.InnerText | Should -Match 'DisableWindowsConsumerFeatures'
$postInstallFile.InnerText | Should -Match 'Microsoft Compatibility Appraiser'
$postInstallFile.InnerText | Should -Match 'OneDriveSetup.exe'
$postInstallFile.InnerText | Should -Match 'function Set-WinUtilContentDeliveryManagerValues'
$postInstallFile.InnerText | Should -Match ([regex]::Escape('Set-WinUtilContentDeliveryManagerValues $defaultHive'))
$postInstallFile.InnerText | Should -Match ([regex]::Escape("Set-WinUtilContentDeliveryManagerValues 'HKCU'"))
$postInstallFile.InnerText | Should -Match ([regex]::Escape("Set-WinUtilRegistryValue 'HKCU\Control Panel\UnsupportedHardwareNotificationCache' 'SV1'"))
$postInstallFile.InnerText | Should -Match ([regex]::Escape("Set-WinUtilRegistryValue 'HKCU\Control Panel\UnsupportedHardwareNotificationCache' 'SV2'"))
foreach ($defaultProfilePath in @(
'$defaultHive\Software\Microsoft\Windows\CurrentVersion\AdvertisingInfo',
'$defaultHive\Software\Microsoft\Windows\CurrentVersion\Privacy',
'$defaultHive\Software\Microsoft\Speech_OneCore\Settings\OnlineSpeechPrivacy',
'$defaultHive\Software\Microsoft\Input\TIPC',
'$defaultHive\Software\Microsoft\InputPersonalization',
'$defaultHive\Software\Microsoft\InputPersonalization\TrainedDataStore',
'$defaultHive\Software\Microsoft\Personalization\Settings'
)) {
$postInstallFile.InnerText | Should -Match ([regex]::Escape($defaultProfilePath))
}
$firstLogonFile = $answerFile.SelectSingleNode('//sg:File[@path="C:\Windows\Setup\Scripts\FirstLogon.ps1"]', $nsMgr)
$firstLogonFile.InnerText | Should -Match 'WinUtil-PostInstall.ps1'
$setupScriptsRoot = Join-Path $contentRoot 'sources\$OEM$\$$\Setup\Scripts'
Test-Path (Join-Path $setupScriptsRoot 'Specialize.ps1') | Should -BeTrue
Test-Path (Join-Path $setupScriptsRoot 'DefaultUser.ps1') | Should -BeTrue
Test-Path (Join-Path $setupScriptsRoot 'FirstLogon.ps1') | Should -BeTrue
Test-Path (Join-Path $setupScriptsRoot 'WinUtil-PostInstall.ps1') | Should -BeTrue
Get-Content -Path (Join-Path $setupScriptsRoot 'FirstLogon.ps1') -Raw | Should -Match 'WinUtil-PostInstall.ps1'
Get-Content -Path (Join-Path $setupScriptsRoot 'WinUtil-PostInstall.ps1') -Raw | Should -Match 'Remove-AppxProvisionedPackage'
$tokens = $null
$errors = $null
[System.Management.Automation.Language.Parser]::ParseInput($postInstallFile.InnerText, [ref]$tokens, [ref]$errors) | Out-Null
$errors.Count | Should -Be 0
} finally {
Remove-Item -Path $contentRoot -Recurse -Force -ErrorAction SilentlyContinue
}
}
It "converts autounattend setup metadata without preserving placeholder product keys" {
. ([scriptblock]::Create($script:answerFileChildElementFunction))
. ([scriptblock]::Create($script:answerFileConversionFunction))
[xml]$templateXml = Get-Content -Path $script:autoUnattendPath -Raw
$unattendNs = "urn:schemas-microsoft-com:unattend"
$nsMgr = New-Object System.Xml.XmlNamespaceManager($templateXml.NameTable)
$nsMgr.AddNamespace("u", $unattendNs)
$nsMgr.AddNamespace("sg", "https://schneegans.de/windows/unattend-generator/")
$setupComponent = $templateXml.SelectSingleNode('//u:settings[@pass="windowsPE"]/u:component[@name="Microsoft-Windows-Setup"]', $nsMgr)
$userData = $templateXml.CreateElement("UserData", $unattendNs)
$productKey = $templateXml.CreateElement("ProductKey", $unattendNs)
$key = $templateXml.CreateElement("Key", $unattendNs)
$key.InnerText = "00000-00000-00000-00000-00000"
[void]$productKey.AppendChild($key)
[void]$userData.AppendChild($productKey)
[void]$setupComponent.AppendChild($userData)
$originalSetupFileCount = $templateXml.SelectNodes("//sg:File", $nsMgr).Count
[xml]$converted = ConvertTo-WinUtilISOAnswerFile -XmlContent $templateXml.OuterXml -ImageIndex 3
$convertedNsMgr = New-Object System.Xml.XmlNamespaceManager($converted.NameTable)
$convertedNsMgr.AddNamespace("u", $unattendNs)
$convertedNsMgr.AddNamespace("sg", "https://schneegans.de/windows/unattend-generator/")
$converted.DocumentElement.NamespaceURI | Should -Be $unattendNs
$converted.DocumentElement.GetAttribute("xmlns:wcm") | Should -Be "http://schemas.microsoft.com/WMIConfig/2002/State"
$converted.SelectNodes('//u:component[@name="Microsoft-Windows-Setup"]/u:UserData/u:ProductKey', $convertedNsMgr).Count | Should -Be 0
$converted.SelectSingleNode('//u:ImageInstall/u:OSImage/u:InstallFrom/u:MetaData[u:Key="/IMAGE/INDEX"]/u:Value', $convertedNsMgr).InnerText | Should -Be "3"
$converted.SelectNodes("//sg:File", $convertedNsMgr).Count | Should -Be $originalSetupFileCount
}
It "logs DISM output for driver injection through a mocked command" {
. ([scriptblock]::Create($script:addDriversFunction))
$script:dismArguments = @()
function dism {
param([Parameter(ValueFromRemainingArguments)]$Arguments)
$script:dismArguments = @($Arguments)
"driver one"
"driver two"
}
It "stages storage drivers for WinPE and adds all drivers to one install.wim index" {
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoDrivers_$([guid]::NewGuid())"
$installWim = Join-Path $contentRoot 'sources\install.wim'
$template = Get-Content -Path $script:autoUnattendPath -Raw
$logs = [System.Collections.Generic.List[string]]::new()
Add-DriversToImage -MountPath "C:\Mount" -DriverDir "C:\Drivers" -Label "install" -Logger {
param($message)
$logs.Add([string]$message)
$script:dismCalls = [System.Collections.Generic.List[string]]::new()
function dism.exe {
param([Parameter(ValueFromRemainingArguments)][string[]]$Arguments)
$script:dismCalls.Add(($Arguments -join '|'))
$global:LASTEXITCODE = 0
if ($Arguments -contains '/Get-WimInfo') {
'Languages : en-US'
'Installation : Client'
'Edition : Professional'
'ProductSuite : Terminal Server'
'ProductType : WinNT'
} elseif ($Arguments -contains '/Mount-Image') {
'[==========================100.0%==========================]'
}
}
($script:dismArguments -join "|") | Should -Be "/English|/image:C:\Mount|/Add-Driver|/Driver:C:\Drivers|/Recurse"
($logs -join "|") | Should -Be " dism[install]: driver one| dism[install]: driver two"
Mock Start-Process {
param($FilePath, $ArgumentList)
if ($FilePath -ne 'dism.exe') {
throw "Unexpected process in driver export mock: $FilePath"
}
$destinationMatch = [regex]::Match([string]$ArgumentList, '/destination:"([^"]+)"')
if (-not $destinationMatch.Success) {
throw "Unable to find the mocked DISM export destination in: $ArgumentList"
}
$exportRoot = $destinationMatch.Groups[1].Value
$fixtures = @(
@{ Path = 'system_pkg'; Name = 'chipset.inf'; Class = 'System' },
@{ Path = 'storage_pkg'; Name = 'iaStorAC.inf'; Class = 'System' },
@{ Path = 'scsi_pkg'; Name = 'controller.inf'; Class = 'SCSIAdapter' },
@{ Path = 'net_pkg'; Name = 'network.inf'; Class = 'Net' },
@{ Path = 'group_a\duplicate'; Name = 'audio.inf'; Class = 'Media' },
@{ Path = 'group_b\duplicate'; Name = 'extension.inf'; Class = 'Extension' }
)
foreach ($fixture in $fixtures) {
$fixturePath = Join-Path $exportRoot $fixture.Path
New-Item -Path $fixturePath -ItemType Directory -Force | Out-Null
Set-Content -Path (Join-Path $fixturePath $fixture.Name) -Value "[Version]`r`nClass=$($fixture.Class)" -Encoding ASCII
}
return [pscustomobject]@{ ExitCode = 0 }
} -ParameterFilter { $FilePath -eq 'dism.exe' }
try {
New-Item -Path (Split-Path $installWim -Parent) -ItemType Directory -Force | Out-Null
Set-Content -Path $installWim -Value 'mock-wim'
. $script:isoScriptPath
Invoke-WinUtilISOScript -ISOContentsDir $contentRoot -AutoUnattendXml $template -InjectCurrentSystemDrivers $true -InstallImagePath $installWim -InstallImageIndex 6 -InstallEditionId 'Professional' -Log {
param($message)
$logs.Add([string]$message)
}
$winpeDriverRoot = Join-Path $contentRoot '$WinpeDriver$'
@(Get-ChildItem -Path $winpeDriverRoot -Directory).Count | Should -Be 2
Test-Path (Join-Path $winpeDriverRoot 'system_pkg\chipset.inf') | Should -BeFalse
Test-Path (Join-Path $winpeDriverRoot 'storage_pkg\iaStorAC.inf') | Should -BeTrue
Test-Path (Join-Path $winpeDriverRoot 'scsi_pkg\controller.inf') | Should -BeTrue
Test-Path (Join-Path $winpeDriverRoot 'net_pkg\network.inf') | Should -BeFalse
@($script:dismCalls | Where-Object { $_ -match '/Mount-Image' }).Count | Should -Be 1
@($script:dismCalls | Where-Object { $_ -match '/Add-Driver' }).Count | Should -Be 1
@($script:dismCalls | Where-Object { $_ -match '/Unmount-Image\|.*\|/Commit' }).Count | Should -Be 1
@($script:dismCalls | Where-Object { $_ -match '/Get-WimInfo' }).Count | Should -Be 2
($script:dismCalls -join "`n") | Should -Not -Match '/Cleanup-Image|/Export-Image'
[xml]$answerFile = Get-Content -Path (Join-Path $contentRoot 'autounattend.xml') -Raw
$nsMgr = New-Object System.Xml.XmlNamespaceManager($answerFile.NameTable)
$nsMgr.AddNamespace('sg', 'https://schneegans.de/windows/unattend-generator/')
$answerFile.SelectSingleNode('//sg:File[@path="C:\Windows\Setup\Scripts\WinUtil-InstallDrivers.ps1"]', $nsMgr) | Should -BeNullOrEmpty
($logs -join '|') | Should -Match 'staged 2 boot-storage packages for WinPE'
($logs -join '|') | Should -Match 'install.wim metadata validation passed'
($logs -join '|') | Should -Match 'DISM mount completed.'
($logs -join '|') | Should -Not -Match '100.0%'
} finally {
Remove-Item Function:\dism.exe -ErrorAction SilentlyContinue
Remove-Item -Path $contentRoot -Recurse -Force -ErrorAction SilentlyContinue
}
}
It "keeps driver export and boot.wim injection behind the selected branch" {
$content = Get-Content -Path $script:isoScriptPath -Raw
It "discards a partially mounted install.wim after mount failure" {
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoMountFailure_$([guid]::NewGuid())"
$installWim = Join-Path $contentRoot 'sources\install.wim'
$template = Get-Content -Path $script:autoUnattendPath -Raw
$script:dismCalls = [System.Collections.Generic.List[string]]::new()
foreach ($expectedText in @(
'if ($InjectCurrentSystemDrivers)',
'Export-WindowsDriver -Online -Destination $driverExportRoot',
'Add-DriversToImage -MountPath $ScratchDir -DriverDir $driverExportRoot -Label "install" -Logger $Log',
'Invoke-BootWimInject -BootWimPath $bootWim -DriverDir $driverExportRoot -Logger $Log',
'Warning: boot.wim not found - skipping boot.wim driver injection.',
'Driver injection skipped.'
)) {
$content | Should -Match ([regex]::Escape($expectedText))
function dism.exe {
param([Parameter(ValueFromRemainingArguments)][string[]]$Arguments)
$script:dismCalls.Add(($Arguments -join '|'))
if ($Arguments -contains '/Get-WimInfo') {
$global:LASTEXITCODE = 0
'Languages : en-US'
'Installation : Client'
'Edition : Professional'
'ProductSuite : Terminal Server'
'ProductType : WinNT'
} elseif ($Arguments -contains '/Mount-Image') {
$global:LASTEXITCODE = 50
'Mount failed'
} elseif ($Arguments -contains '/Get-MountedImageInfo') {
$global:LASTEXITCODE = 0
"Mount Dir : $(Join-Path (Split-Path -Path $contentRoot -Parent) 'wim_mount')"
} else {
$global:LASTEXITCODE = 0
}
}
Mock Start-Process {
param($FilePath, $ArgumentList)
$destinationMatch = [regex]::Match([string]$ArgumentList, '/destination:"([^"]+)"')
$exportRoot = $destinationMatch.Groups[1].Value
$fixturePath = Join-Path $exportRoot 'storage_pkg'
New-Item -Path $fixturePath -ItemType Directory -Force | Out-Null
Set-Content -Path (Join-Path $fixturePath 'iaStorAC.inf') -Value "[Version]`r`nClass=System" -Encoding ASCII
return [pscustomobject]@{ ExitCode = 0 }
} -ParameterFilter { $FilePath -eq 'dism.exe' }
try {
New-Item -Path (Split-Path $installWim -Parent) -ItemType Directory -Force | Out-Null
Set-Content -Path $installWim -Value 'mock-wim'
. $script:isoScriptPath
{ Invoke-WinUtilISOScript -ISOContentsDir $contentRoot -AutoUnattendXml $template -InjectCurrentSystemDrivers $true -InstallImagePath $installWim -InstallImageIndex 6 -InstallEditionId 'Professional' } |
Should -Throw '*DISM mount failed*'
@($script:dismCalls | Where-Object { $_ -match '/Get-MountedImageInfo' }).Count | Should -Be 1
@($script:dismCalls | Where-Object { $_ -match '/Unmount-Image\|.*\|/Discard' }).Count | Should -Be 1
} finally {
Remove-Item Function:\dism.exe -ErrorAction SilentlyContinue
Remove-Item -Path $contentRoot -Recurse -Force -ErrorAction SilentlyContinue
}
}
It "does not add driver setup artifacts when injection is disabled" {
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoNoDrivers_$([guid]::NewGuid())"
try {
New-Item -Path $contentRoot -ItemType Directory -Force | Out-Null
. $script:isoScriptPath
Invoke-WinUtilISOScript -ISOContentsDir $contentRoot -AutoUnattendXml (Get-Content -Path $script:autoUnattendPath -Raw) -InjectCurrentSystemDrivers $false -InstallEditionId 'Core'
Test-Path (Join-Path $contentRoot '$WinpeDriver$') | Should -BeFalse
[xml]$answerFile = Get-Content -Path (Join-Path $contentRoot 'autounattend.xml') -Raw
$nsMgr = New-Object System.Xml.XmlNamespaceManager($answerFile.NameTable)
$nsMgr.AddNamespace('sg', 'https://schneegans.de/windows/unattend-generator/')
$answerFile.SelectSingleNode('//sg:File[@path="C:\Windows\Setup\Scripts\WinUtil-InstallDrivers.ps1"]', $nsMgr) | Should -BeNullOrEmpty
} finally {
Remove-Item -Path $contentRoot -Recurse -Force -ErrorAction SilentlyContinue
}
}
It "enables configuration-set fallback when staging OEM setup scripts" {
$contentRoot = Join-Path ([IO.Path]::GetTempPath()) "WinUtilIsoFallback_$([guid]::NewGuid())"
try {
New-Item -Path $contentRoot -ItemType Directory -Force | Out-Null
. $script:isoScriptPath
Invoke-WinUtilISOScript -ISOContentsDir $contentRoot -AutoUnattendXml (Get-Content -Path $script:autoUnattendPath -Raw) -InstallEditionId 'Core'
[xml]$answerFile = Get-Content -Path (Join-Path $contentRoot 'autounattend.xml') -Raw
$nsMgr = New-Object System.Xml.XmlNamespaceManager($answerFile.NameTable)
$nsMgr.AddNamespace('u', 'urn:schemas-microsoft-com:unattend')
$answerFile.SelectSingleNode('/u:unattend/u:settings[@pass="windowsPE"]/u:component[@name="Microsoft-Windows-Setup"]/u:UseConfigurationSet', $nsMgr).InnerText |
Should -Be 'true'
Test-Path (Join-Path $contentRoot 'sources\$OEM$\$$\Setup\Scripts\FirstLogon.ps1') | Should -BeTrue
} finally {
Remove-Item -Path $contentRoot -Recurse -Force -ErrorAction SilentlyContinue
}
}
+149 -9
View File
@@ -134,20 +134,31 @@ Describe "XAML document" {
"WPFTab3BT",
"WPFTab4BT",
"WPFTab5BT",
"WPFTab6BT",
"SearchBar",
"SearchBarIcon",
"SearchBarClearButton",
"WPFSearchChips",
"WPFSearchChipAll",
"WPFSearchChipBrowsers",
"WPFSearchChipCommunications",
"WPFSearchChipDevelopment",
"WPFSearchChipGames",
"WPFSearchChipMicrosoftTools",
"WPFSearchChipMultimediaTools",
"WPFSearchChipProTools",
"WPFSearchChipSelfhostedTools",
"WPFSearchChipUtilities",
"appscategory",
"appspanel",
"tweakspanel",
"featurespanel",
"updatespanel",
"appxpanel",
"WPFstandard",
"WPFminimal",
"WPFAdvanced",
"WPFClearTweaksSelection",
"WPFGetInstalledTweaks",
"WPFAppxRemoval",
"WPFTweaksbutton",
"WPFUndoall",
"WPFUpdatesdefault",
@@ -157,6 +168,8 @@ Describe "XAML document" {
"WPFGetInstalledAppx",
"WPFSelectAllAppx",
"WPFClearAppxSelection",
"WPFBackToTweaks",
"WPFInstallSelectedAppx",
"WPFRemoveSelectedAppx"
)
@@ -166,6 +179,23 @@ Describe "XAML document" {
}
}
It "presents the three Updates profiles with accurate action labels" {
$updatesTab = $script:xaml.SelectSingleNode('//*[local-name()="TabItem"][@Name="WPFTab4"]')
$profileGrid = $updatesTab.SelectSingleNode('.//*[local-name()="UniformGrid"]')
$expectedButtons = @{
WPFUpdatessecurity = "Apply Recommended"
WPFUpdatesdefault = "Restore Defaults"
WPFUpdatesdisable = "Disable Updates"
}
$profileGrid.GetAttribute("Columns") | Should -Be "3"
foreach ($buttonName in $expectedButtons.Keys) {
$button = $updatesTab.SelectSingleNode(".//*[local-name()='Button'][@Name='$buttonName']")
$button.GetAttribute("Content") | Should -Be $expectedButtons[$buttonName]
}
$updatesTab.SelectSingleNode('.//*[@Name="updatespanel"]') | Should -BeNullOrEmpty
}
It "contains Win11 Creator controls used by the ISO workflow" {
$xamlNames = @(Get-WinUtilXamlRuntimeNamedControls | ForEach-Object { $_.Name })
$requiredControls = @(
@@ -223,6 +253,122 @@ Describe "XAML document" {
}
}
}
It "opens AppX removal from Tweaks and provides a return path" {
$navPanel = $script:xaml.SelectSingleNode('//*[local-name()="StackPanel"][@Name="NavDockPanel"]')
$tweaksTab = $script:xaml.SelectSingleNode('//*[local-name()="TabItem"][@Name="WPFTab2"]')
$appxTab = $script:xaml.SelectSingleNode('//*[local-name()="TabItem"][@Name="WPFTab6"]')
$openButton = $tweaksTab.SelectSingleNode('.//*[local-name()="Button"][@Name="WPFAppxRemoval"]')
$buttonNames = @($openButton.ParentNode.SelectNodes('./*[local-name()="Button"]') | ForEach-Object { $_.GetAttribute("Name") })
$getInstalledIndex = [array]::IndexOf($buttonNames, "WPFGetInstalledTweaks")
$openAppxIndex = [array]::IndexOf($buttonNames, "WPFAppxRemoval")
$buttonSource = Get-Content -Path $script:buttonScriptPath -Raw
$tabSource = Get-Content -Path (Join-Path $script:functionRoot "public\Invoke-WPFTab.ps1") -Raw
$navPanel.SelectSingleNode('./*[local-name()="ToggleButton"][@Name="WPFTab6BT"]') | Should -BeNullOrEmpty
$openButton.GetAttribute("Content").Trim() | Should -Be "AppX Removal"
$openAppxIndex | Should -Be ($getInstalledIndex + 1)
$appxTab.SelectSingleNode('.//*[local-name()="Button"][@Name="WPFBackToTweaks"]') | Should -Not -BeNullOrEmpty
$appxTab.SelectSingleNode('.//*[local-name()="Button"][@Name="WPFInstallSelectedAppx"]') | Should -Not -BeNullOrEmpty
$appxTab.SelectSingleNode('.//*[local-name()="Button"][@Name="WPFRemoveSelectedAppx"]') | Should -Not -BeNullOrEmpty
$buttonSource | Should -Match '"WPFAppxRemoval"\s*\{Invoke-WPFTab "WPFTab6BT"\}'
$buttonSource | Should -Match '"WPFBackToTweaks"\s*\{Invoke-WPFTab "WPFTab2BT"\}'
$buttonSource | Should -Match '"WPFInstallSelectedAppx"\s*\{Invoke-WPFAppxInstall\}'
$tabSource | Should -Match '\$sync\.\$tabNav\.Items\[\$tabNumber\]\.IsSelected = \$true'
}
It "centers top bar controls vertically" {
$navPanel = $script:xaml.SelectSingleNode('//*[local-name()="StackPanel"][@Name="NavDockPanel"]')
$minimizeButton = $script:xaml.SelectSingleNode('//*[local-name()="Button"][@Name="WPFMinimizeButton"]')
$actionPanel = $minimizeButton.ParentNode
$topBarButtonNames = @(
"ThemeButton",
"FontScalingButton",
"SettingsButton",
"WPFMinimizeButton",
"WPFMaximizeButton",
"WPFCloseButton"
)
$navPanel.GetAttribute("VerticalAlignment") | Should -Be "Center"
$actionPanel.GetAttribute("VerticalAlignment") | Should -Be "Center"
foreach ($buttonName in $topBarButtonNames) {
$button = $script:xaml.SelectSingleNode("//*[local-name()='Button'][@Name='$buttonName']")
$button.GetAttribute("VerticalAlignment") | Should -Be "Center"
}
}
It "keeps the responsive search controls within the available screen width" {
$window = $script:xaml.DocumentElement
$searchBar = $script:xaml.SelectSingleNode('//*[local-name()="TextBox"][@Name="SearchBar"]')
$searchBorder = $searchBar.ParentNode.ParentNode
$mainScript = Get-Content -Path $script:mainScriptPath -Raw
$window.GetAttribute("MinWidth") | Should -Be "800"
$searchBorder.GetAttribute("Width") | Should -BeNullOrEmpty
$searchBorder.GetAttribute("HorizontalAlignment") | Should -Be "Stretch"
$searchBar.GetAttribute("Width") | Should -BeNullOrEmpty
$searchBar.GetAttribute("HorizontalAlignment") | Should -Be "Stretch"
$mainScript | Should -Match '\$sync\.Form\.MinWidth = "1150"'
$mainScript | Should -Match '\$sync\.Form\.MinWidth = \[Math\]::Min\(\[double\]\$sync\.Form\.MinWidth, \[double\]\$screenWidth\)'
}
It "shows only one search action glyph at a time" {
$searchIcon = $script:xaml.SelectSingleNode('//*[local-name()="TextBlock"][@Name="SearchBarIcon"]')
$clearButton = $script:xaml.SelectSingleNode('//*[local-name()="Button"][@Name="SearchBarClearButton"]')
$mainScript = Get-Content -Path $script:mainScriptPath -Raw
$searchIcon | Should -Not -BeNullOrEmpty
$clearButton | Should -Not -BeNullOrEmpty
$mainScript | Should -Match '\$sync\.SearchBarClearButton\.Visibility = "Visible"\s+\$sync\.SearchBarIcon\.Visibility = "Collapsed"'
$mainScript | Should -Match '\$sync\.SearchBarClearButton\.Visibility = "Collapsed"\s+\$sync\.SearchBarIcon\.Visibility = "Visible"'
}
It "scopes toggle button styles without leaking into combo boxes" {
$resources = $script:xaml.SelectSingleNode('//*[local-name()="Window.Resources"]')
$implicitToggleStyles = @($resources.SelectNodes('./*[local-name()="Style"][@TargetType="ToggleButton" or @TargetType="{x:Type ToggleButton}"][not(@x:Key)]', $script:xamlNamespace))
$tabStyle = $resources.SelectSingleNode('./*[local-name()="Style"][@x:Key="TabToggleButton"]', $script:xamlNamespace)
$comboToggleStyle = $resources.SelectSingleNode('./*[local-name()="Style"][@x:Key="ComboBoxToggleButtonStyle"]', $script:xamlNamespace)
$comboStyle = $resources.SelectSingleNode('./*[local-name()="Style"][@TargetType="ComboBox"]')
$comboToggle = $comboStyle.SelectSingleNode('.//*[local-name()="ToggleButton"][@Name="ToggleButton"]')
$comboItemStyle = $resources.SelectSingleNode('./*[local-name()="Style"][@TargetType="ComboBoxItem"]')
$navButtons = @($script:xaml.SelectNodes('//*[local-name()="StackPanel"][@Name="NavDockPanel"]/*[local-name()="ToggleButton"]'))
$implicitToggleStyles | Should -BeNullOrEmpty
$tabStyle | Should -Not -BeNullOrEmpty
$comboToggleStyle | Should -Not -BeNullOrEmpty
$comboToggle.GetAttribute("Style") | Should -Be "{StaticResource ComboBoxToggleButtonStyle}"
$comboItemStyle | Should -Not -BeNullOrEmpty
$navButtons.Count | Should -Be 5
foreach ($navButton in $navButtons) {
$navButton.GetAttribute("Style") | Should -Be "{StaticResource TabToggleButton}"
}
}
It "uses state-aware maximize and restore icons" {
$themes = Get-WinUtilConfigObject -Name "themes"
$minimizeButton = $script:xaml.SelectSingleNode('//*[local-name()="Button"][@Name="WPFMinimizeButton"]')
$maximizeButton = $script:xaml.SelectSingleNode('//*[local-name()="Button"][@Name="WPFMaximizeButton"]')
$closeButton = $script:xaml.SelectSingleNode('//*[local-name()="Button"][@Name="WPFCloseButton"]')
$maximizeStyle = $maximizeButton.SelectSingleNode('./*[local-name()="Button.Style"]/*[local-name()="Style"]')
$maximizeIcon = $maximizeStyle.SelectSingleNode('./*[local-name()="Setter"][@Property="Content"]')
$maximizedTrigger = $maximizeStyle.SelectSingleNode('./*[local-name()="Style.Triggers"]/*[local-name()="DataTrigger"][@Value="Maximized"]')
$restoreIcon = $maximizedTrigger.SelectSingleNode('./*[local-name()="Setter"][@Property="Content"]')
foreach ($button in @($minimizeButton, $maximizeButton, $closeButton)) {
$button.GetAttribute("FontFamily") | Should -Be "Segoe MDL2 Assets"
$button.GetAttribute("FontSize") | Should -Be "{DynamicResource CloseIconFontSize}"
$button.GetAttribute("Margin") | Should -BeIn @("0", "0,0,0,0")
}
$minimizeButton.GetAttribute("Content") | Should -Be ([string][char]0xE921)
$maximizeIcon.GetAttribute("Value") | Should -Be ([string][char]0xE922)
$restoreIcon.GetAttribute("Value") | Should -Be ([string][char]0xE923)
$closeButton.GetAttribute("Content") | Should -Be ([string][char]0xE8BB)
$maximizeStyle.GetAttribute("BasedOn") | Should -Be "{StaticResource HoverButtonStyle}"
[int]$themes.shared.CloseIconFontSize | Should -BeLessThan ([int]$themes.shared.SettingsIconFontSize)
}
}
Describe "XAML and sync wiring" {
@@ -289,7 +435,6 @@ Describe "XAML and sync wiring" {
"appPopup",
"appPopupSelectedApp",
"ItemsControl",
"InstalledPrograms",
"ImportInProgress",
"ScriptsInstallPrograms",
"keys",
@@ -302,20 +447,15 @@ Describe "XAML and sync wiring" {
"InitializedTabs",
"RenderedAssetCache",
"ToggleStatusCache",
"InstallAppAreaBorder",
"InstallAppAreaScrollViewer",
"InstallAppAreaOverlay",
"InstallAppAreaOverlayText",
"InstallAppRenderQueue",
"InstallAppEntriesRendered",
"ProgressBar",
"progressBarTextBlock",
"FontScaleFactor",
"Win11ISOImageInfo",
"Win11ISODriveLetter",
"Win11ISOWimPath",
"Win11ISOImagePath",
"Win11ISOModifying",
"Win11ISOProcessRunning",
"Win11ISOWorkDir",
"Win11ISOContentsDir",
"Win11ISOUSBDisks"
+27 -14
View File
@@ -128,9 +128,6 @@ Invoke-WinutilThemeChange -theme $sync.preferences.theme
$sync.InitializedTabs = @{}
Initialize-WinUtilTabContent -TabName "Install"
# Future implementation: Add Windows Version to updates panel
#Invoke-WPFUIElements -configVariable $sync.configs.updates -targetGridName "updatespanel" -columncount 1
#===========================================================================
# Store Form Objects In PowerShell
#===========================================================================
@@ -171,15 +168,6 @@ $sync.keys | ForEach-Object {
}
}
if ($($sync["$psitem"].GetType() | Select-Object -ExpandProperty Name) -eq "TextBlock") {
if ($sync["$psitem"].Name.EndsWith("Link")) {
$sync["$psitem"].Add_MouseUp({
[System.Object]$Sender = $args[0]
Start-Process $Sender.ToolTip -ErrorAction Stop
})
}
}
}
}
@@ -285,6 +273,7 @@ $sync["Form"].Add_ContentRendered({
# Extract screen width and height for the primary monitor
$screenWidth = $primaryScreen.Bounds.Width
$screenHeight = $primaryScreen.Bounds.Height
$sync.Form.MinWidth = [Math]::Min([double]$sync.Form.MinWidth, [double]$screenWidth)
# Compare with the primary monitor size
if ($sync.Form.ActualWidth -gt $screenWidth -or $sync.Form.ActualHeight -gt $screenHeight) {
@@ -340,7 +329,7 @@ $searchBarTimer.add_Tick({
$searchBarTimer.Stop()
switch ($sync.currentTab) {
"Install" {
Find-AppsByNameOrDescription -SearchString $sync.SearchBar.Text
Find-AppsByNameOrDescription -SearchString $sync.SearchBar.Text -Category $sync.SearchBar.Tag
}
"Tweaks" {
Find-TweaksByNameOrDescription -SearchString $sync.SearchBar.Text
@@ -351,21 +340,45 @@ $searchBarTimer.add_Tick({
}
})
$sync["SearchBar"].Add_TextChanged({
if ($sync.SearchBar.Tag -ne $sync.SearchBar.Text) {
$sync.SearchBar.Tag = $null
}
if ($sync.SearchBar.Text -ne "") {
$sync.SearchBarClearButton.Visibility = "Visible"
$sync.SearchBarIcon.Visibility = "Collapsed"
} else {
$sync.SearchBarClearButton.Visibility = "Collapsed"
$sync.SearchBarIcon.Visibility = "Visible"
}
# Category chip handlers apply their filter immediately.
if ($sync.SearchBar.Tag -eq $sync.SearchBar.Text) {
return
}
if ($searchBarTimer.IsEnabled) {
$searchBarTimer.Stop()
}
$searchBarTimer.Start()
})
# Quick Category Search Chips
$sync["WPFSearchChipAll"].Add_Click({ Set-WinUtilAppCategoryFilter })
$sync["WPFSearchChipBrowsers"].Add_Click({ Set-WinUtilAppCategoryFilter -Category "Browsers" })
$sync["WPFSearchChipCommunications"].Add_Click({ Set-WinUtilAppCategoryFilter -Category "Communications" })
$sync["WPFSearchChipDevelopment"].Add_Click({ Set-WinUtilAppCategoryFilter -Category "Development" })
$sync["WPFSearchChipGames"].Add_Click({ Set-WinUtilAppCategoryFilter -Category "Games" })
$sync["WPFSearchChipMicrosoftTools"].Add_Click({ Set-WinUtilAppCategoryFilter -Category "Microsoft Tools" })
$sync["WPFSearchChipMultimediaTools"].Add_Click({ Set-WinUtilAppCategoryFilter -Category "Multimedia Tools" })
$sync["WPFSearchChipProTools"].Add_Click({ Set-WinUtilAppCategoryFilter -Category "Pro Tools" })
$sync["WPFSearchChipSelfhostedTools"].Add_Click({ Set-WinUtilAppCategoryFilter -Category "Selfhosted Tools" })
$sync["WPFSearchChipUtilities"].Add_Click({ Set-WinUtilAppCategoryFilter -Category "Utilities" })
$sync["Form"].Add_Loaded({
param($e)
$null = $e
$sync.Form.MinWidth = "1000"
$sync.Form.MinWidth = "1150"
$sync["Form"].MaxWidth = [Double]::PositiveInfinity
$sync["Form"].MaxHeight = [Double]::PositiveInfinity
})
+1
View File
@@ -62,6 +62,7 @@ $sync.configs = @{}
$sync.Buttons = [System.Collections.Generic.List[PSObject]]::new()
$sync.preferences = @{}
$sync.ProcessRunning = $false
$sync.Win11ISOProcessRunning = $false
$sync.selectedAppx = [System.Collections.Generic.List[string]]::new()
$sync.selectedApps = [System.Collections.Generic.List[string]]::new()
$sync.selectedTweaks = [System.Collections.Generic.List[string]]::new()
+20
View File
@@ -21,6 +21,14 @@
<Order>3</Order>
<Path>reg.exe add "HKLM\SYSTEM\Setup\LabConfig" /v BypassRAMCheck /t REG_DWORD /d 1 /f</Path>
</RunSynchronousCommand>
<RunSynchronousCommand wcm:action="add">
<Order>4</Order>
<Path>reg.exe add "HKLM\SYSTEM\Setup\LabConfig" /v BypassCPUCheck /t REG_DWORD /d 1 /f</Path>
</RunSynchronousCommand>
<RunSynchronousCommand wcm:action="add">
<Order>5</Order>
<Path>reg.exe add "HKLM\SYSTEM\Setup\LabConfig" /v BypassStorageCheck /t REG_DWORD /d 1 /f</Path>
</RunSynchronousCommand>
</RunSynchronous>
</component>
</settings>
@@ -48,6 +56,18 @@
<Order>5</Order>
<Path>reg.exe unload "HKU\DefaultUser"</Path>
</RunSynchronousCommand>
<RunSynchronousCommand wcm:action="add">
<Order>6</Order>
<Path>reg.exe add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE" /v BypassNRO /t REG_DWORD /d 1 /f</Path>
</RunSynchronousCommand>
<RunSynchronousCommand wcm:action="add">
<Order>7</Order>
<Path>reg.exe add "HKLM\SYSTEM\CurrentControlSet\Control\BitLocker" /v PreventDeviceEncryption /t REG_DWORD /d 1 /f</Path>
</RunSynchronousCommand>
<RunSynchronousCommand wcm:action="add">
<Order>8</Order>
<Path>reg.exe add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\ReserveManager" /v ShippedWithReserves /t REG_DWORD /d 0 /f</Path>
</RunSynchronousCommand>
</RunSynchronous>
</component>
</settings>
+322 -126
View File
@@ -6,7 +6,7 @@
xmlns:local="clr-namespace:WinUtility"
WindowStartupLocation="CenterScreen"
UseLayoutRounding="True"
WindowStyle="None"
WindowStyle="SingleBorderWindow"
Width="Auto"
Height="Auto"
MinWidth="800"
@@ -248,6 +248,17 @@
</Setter.Value>
</Setter>
</Style>
<Style x:Key="ComboBoxToggleButtonStyle" TargetType="ToggleButton">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ToggleButton">
<Border Background="{TemplateBinding Background}" BorderThickness="0">
<ContentPresenter/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="ComboBox">
<Setter Property="Foreground" Value="{DynamicResource ComboBoxForegroundColor}" />
<Setter Property="Background" Value="{DynamicResource ComboBoxBackgroundColor}" />
@@ -262,6 +273,7 @@
CornerRadius="{DynamicResource ButtonCornerRadius}"
Background="{TemplateBinding Background}">
<ToggleButton Name="ToggleButton"
Style="{StaticResource ComboBoxToggleButtonStyle}"
Background="Transparent"
BorderThickness="0"
IsChecked="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}"
@@ -309,6 +321,27 @@
</Setter.Value>
</Setter>
</Style>
<Style TargetType="ComboBoxItem">
<Setter Property="Background" Value="{DynamicResource ComboBoxBackgroundColor}"/>
<Setter Property="Foreground" Value="{DynamicResource ComboBoxForegroundColor}"/>
<Setter Property="Padding" Value="6,3"/>
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<TextBlock Text="{Binding}" Background="Transparent"
Foreground="{Binding Foreground, RelativeSource={RelativeSource AncestorType=ComboBoxItem}}"/>
</DataTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsHighlighted" Value="True">
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundMouseoverColor}"/>
</Trigger>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundSelectedColor}"/>
</Trigger>
</Style.Triggers>
</Style>
<Style TargetType="Label">
<Setter Property="Foreground" Value="{DynamicResource LabelboxForegroundColor}"/>
<Setter Property="Background" Value="{DynamicResource LabelBackgroundColor}"/>
@@ -321,8 +354,7 @@
<Setter Property="Foreground" Value="{DynamicResource LabelboxForegroundColor}"/>
<Setter Property="Background" Value="{DynamicResource LabelBackgroundColor}"/>
</Style>
<!-- Toggle button template x:Key="TabToggleButton" -->
<Style TargetType="{x:Type ToggleButton}">
<Style x:Key="TabToggleButton" TargetType="{x:Type ToggleButton}">
<Setter Property="Margin" Value="{DynamicResource ButtonMargin}"/>
<Setter Property="Content" Value=""/>
<Setter Property="FontFamily" Value="{DynamicResource FontFamily}"/>
@@ -918,12 +950,60 @@
</MultiDataTrigger>
</Style.Triggers>
</Style>
<Style x:Key="RoundedProgressBarStyle" TargetType="ProgressBar">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ProgressBar">
<Border CornerRadius="4" Background="{DynamicResource MainBackgroundColor}" BorderBrush="{DynamicResource MainForegroundColor}" BorderThickness="1">
<Grid ClipToBounds="True">
<Border Name="PART_Track" CornerRadius="4" Background="Transparent"/>
<Border Name="PART_Indicator" CornerRadius="4" Background="{DynamicResource ProgressBarForegroundColor}" HorizontalAlignment="Left"/>
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- Filter Chip Style — used by the Install tab category filter buttons -->
<Style x:Key="FilterChipStyle" TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
<Setter Property="Margin" Value="2"/>
<Setter Property="Padding" Value="12,0,12,0"/>
<Setter Property="Width" Value="Auto"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border Name="ChipBorder"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{DynamicResource ButtonBorderThickness}"
CornerRadius="{DynamicResource ButtonCornerRadius}"
Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="ChipBorder" Property="Background" Value="{DynamicResource ButtonBackgroundPressedColor}"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="ChipBorder" Property="Background" Value="{DynamicResource ButtonBackgroundMouseoverColor}"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="ChipBorder" Property="Background" Value="{DynamicResource ButtonBackgroundSelectedColor}"/>
<Setter Property="Foreground" Value="DimGray"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<Grid Background="{DynamicResource MainBackgroundColor}" ShowGridLines="False" Name="WPFMainGrid" Width="Auto" Height="Auto" HorizontalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
@@ -940,10 +1020,10 @@
</Grid.ColumnDefinitions>
<!-- Navigation Buttons Panel -->
<StackPanel Name="NavDockPanel" Orientation="Horizontal" Grid.Column="0" Margin="5,5,10,5">
<StackPanel Name="NavDockPanel" Orientation="Horizontal" Grid.Column="0" VerticalAlignment="Center" Margin="5,5,10,5">
<StackPanel Name="NavLogoPanel" Orientation="Horizontal" HorizontalAlignment="Left" Background="{DynamicResource MainBackgroundColor}" SnapsToDevicePixels="True" Margin="10,0,20,0">
</StackPanel>
<ToggleButton Margin="0,0,5,0" Height="{DynamicResource TabButtonHeight}" Width="{DynamicResource TabButtonWidth}"
<ToggleButton Style="{StaticResource TabToggleButton}" Margin="0,0,5,0" Height="{DynamicResource TabButtonHeight}" Width="{DynamicResource TabButtonWidth}"
Background="{DynamicResource ButtonInstallBackgroundColor}" Foreground="white" FontWeight="Bold" Name="WPFTab1BT">
<ToggleButton.Content>
<TextBlock FontSize="{DynamicResource TabButtonFontSize}" Background="Transparent" Foreground="{DynamicResource ButtonInstallForegroundColor}" >
@@ -951,7 +1031,7 @@
</TextBlock>
</ToggleButton.Content>
</ToggleButton>
<ToggleButton Margin="0,0,5,0" Height="{DynamicResource TabButtonHeight}" Width="{DynamicResource TabButtonWidth}"
<ToggleButton Style="{StaticResource TabToggleButton}" Margin="0,0,5,0" Height="{DynamicResource TabButtonHeight}" Width="{DynamicResource TabButtonWidth}"
Background="{DynamicResource ButtonTweaksBackgroundColor}" Foreground="{DynamicResource ButtonTweaksForegroundColor}" FontWeight="Bold" Name="WPFTab2BT">
<ToggleButton.Content>
<TextBlock FontSize="{DynamicResource TabButtonFontSize}" Background="Transparent" Foreground="{DynamicResource ButtonTweaksForegroundColor}">
@@ -959,7 +1039,7 @@
</TextBlock>
</ToggleButton.Content>
</ToggleButton>
<ToggleButton Margin="0,0,5,0" Height="{DynamicResource TabButtonHeight}" Width="{DynamicResource TabButtonWidth}"
<ToggleButton Style="{StaticResource TabToggleButton}" Margin="0,0,5,0" Height="{DynamicResource TabButtonHeight}" Width="{DynamicResource TabButtonWidth}"
Background="{DynamicResource ButtonConfigBackgroundColor}" Foreground="{DynamicResource ButtonConfigForegroundColor}" FontWeight="Bold" Name="WPFTab3BT">
<ToggleButton.Content>
<TextBlock FontSize="{DynamicResource TabButtonFontSize}" Background="Transparent" Foreground="{DynamicResource ButtonConfigForegroundColor}">
@@ -967,7 +1047,7 @@
</TextBlock>
</ToggleButton.Content>
</ToggleButton>
<ToggleButton Margin="0,0,5,0" Height="{DynamicResource TabButtonHeight}" Width="{DynamicResource TabButtonWidth}"
<ToggleButton Style="{StaticResource TabToggleButton}" Margin="0,0,5,0" Height="{DynamicResource TabButtonHeight}" Width="{DynamicResource TabButtonWidth}"
Background="{DynamicResource ButtonUpdatesBackgroundColor}" Foreground="{DynamicResource ButtonUpdatesForegroundColor}" FontWeight="Bold" Name="WPFTab4BT">
<ToggleButton.Content>
<TextBlock FontSize="{DynamicResource TabButtonFontSize}" Background="Transparent" Foreground="{DynamicResource ButtonUpdatesForegroundColor}">
@@ -975,7 +1055,7 @@
</TextBlock>
</ToggleButton.Content>
</ToggleButton>
<ToggleButton Margin="0,0,5,0" Height="{DynamicResource TabButtonHeight}" Width="Auto" MinWidth="{DynamicResource TabButtonWidth}"
<ToggleButton Style="{StaticResource TabToggleButton}" Margin="0,0,5,0" Height="{DynamicResource TabButtonHeight}" Width="Auto" MinWidth="{DynamicResource TabButtonWidth}"
Background="{DynamicResource ButtonWin11ISOBackgroundColor}" Foreground="{DynamicResource ButtonWin11ISOForegroundColor}" FontWeight="Bold" Name="WPFTab5BT">
<ToggleButton.Content>
<TextBlock FontSize="{DynamicResource TabButtonFontSize}" Background="Transparent" Foreground="{DynamicResource ButtonWin11ISOForegroundColor}">
@@ -983,14 +1063,6 @@
</TextBlock>
</ToggleButton.Content>
</ToggleButton>
<ToggleButton Margin="0,0,5,0" Height="{DynamicResource TabButtonHeight}" Width="Auto" MinWidth="{DynamicResource TabButtonWidth}"
Background="{DynamicResource ButtonAppxBackgroundColor}" Foreground="{DynamicResource ButtonAppxForegroundColor}" FontWeight="Bold" Name="WPFTab6BT">
<ToggleButton.Content>
<TextBlock FontSize="{DynamicResource TabButtonFontSize}" Background="Transparent" Foreground="{DynamicResource ButtonAppxForegroundColor}">
<Underline>A</Underline>ppX Removal
</TextBlock>
</ToggleButton.Content>
</ToggleButton>
</StackPanel>
<!-- Search Bar and Action Buttons -->
@@ -1000,13 +1072,12 @@
<ColumnDefinition Width="Auto"/><!-- Buttons area -->
</Grid.ColumnDefinitions>
<Border Grid.Column="0" Margin="5,0,0,0" Width="{DynamicResource SearchBarWidth}" Height="{DynamicResource SearchBarHeight}" VerticalAlignment="Center" HorizontalAlignment="Left">
<Border Grid.Column="0" Margin="5,0,10,0" MinWidth="120" Height="{DynamicResource SearchBarHeight}" VerticalAlignment="Center" HorizontalAlignment="Stretch">
<Grid>
<TextBox
Width="{DynamicResource SearchBarWidth}"
Height="{DynamicResource SearchBarHeight}"
FontSize="{DynamicResource SearchBarTextBoxFontSize}"
VerticalAlignment="Center" HorizontalAlignment="Left"
VerticalAlignment="Center" HorizontalAlignment="Stretch"
BorderThickness="1"
Name="SearchBar"
Foreground="{DynamicResource MainForegroundColor}" Background="{DynamicResource MainBackgroundColor}"
@@ -1014,6 +1085,7 @@
ToolTip="Press Ctrl-F and type app name to filter application list below. Press Esc to reset the filter">
</TextBox>
<TextBlock
Name="SearchBarIcon"
VerticalAlignment="Center" HorizontalAlignment="Right"
FontFamily="Segoe MDL2 Assets"
Foreground="{DynamicResource ButtonBackgroundSelectedColor}"
@@ -1023,14 +1095,14 @@
</Grid>
</Border>
<Button Grid.Column="0"
VerticalAlignment="Center" HorizontalAlignment="Left"
VerticalAlignment="Center" HorizontalAlignment="Right"
Name="SearchBarClearButton"
Style="{StaticResource SearchBarClearButtonStyle}"
Margin="213,0,0,0" Visibility="Collapsed">
Margin="0,0,20,0" Visibility="Collapsed">
</Button>
<!-- Buttons Container -->
<StackPanel Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Top" Margin="5,5,5,5">
<StackPanel Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Center" Margin="5,5,5,5">
<Button Name="ThemeButton"
Style="{StaticResource HoverButtonStyle}"
BorderBrush="Transparent"
@@ -1038,7 +1110,7 @@
Foreground="{DynamicResource MainForegroundColor}"
FontSize="{DynamicResource SettingsIconFontSize}"
Width="{DynamicResource IconButtonSize}" Height="{DynamicResource IconButtonSize}"
HorizontalAlignment="Right" VerticalAlignment="Top"
HorizontalAlignment="Right" VerticalAlignment="Center"
Margin="0,0,2,0"
FontFamily="Segoe MDL2 Assets"
Content="N/A"
@@ -1076,7 +1148,7 @@
Foreground="{DynamicResource MainForegroundColor}"
FontSize="{DynamicResource SettingsIconFontSize}"
Width="{DynamicResource IconButtonSize}" Height="{DynamicResource IconButtonSize}"
HorizontalAlignment="Right" VerticalAlignment="Top"
HorizontalAlignment="Right" VerticalAlignment="Center"
Margin="0,0,2,0"
FontFamily="Segoe MDL2 Assets"
Content="&#xE8D3;"
@@ -1144,7 +1216,7 @@
Foreground="{DynamicResource MainForegroundColor}"
FontSize="{DynamicResource SettingsIconFontSize}"
Width="{DynamicResource IconButtonSize}" Height="{DynamicResource IconButtonSize}"
HorizontalAlignment="Right" VerticalAlignment="Top"
HorizontalAlignment="Right" VerticalAlignment="Center"
Margin="0,0,2,0"
FontFamily="Segoe MDL2 Assets"
Content="&#xE713;"/>
@@ -1173,24 +1245,62 @@
</Popup>
<Button
Content="&#x2212;" BorderThickness="0"
BorderBrush="Transparent"
Background="{DynamicResource MainBackgroundColor}"
Width="{DynamicResource IconButtonSize}" Height="{DynamicResource IconButtonSize}"
HorizontalAlignment="Right" VerticalAlignment="Top"
Margin="0,0,0,0"
FontFamily="{DynamicResource FontFamily}"
Foreground="{DynamicResource MainForegroundColor}" FontSize="{DynamicResource CloseIconFontSize}" Name="WPFMinimizeButton" />
Content="&#xE921;"
Style="{StaticResource HoverButtonStyle}"
BorderThickness="0"
BorderBrush="Transparent"
Background="{DynamicResource MainBackgroundColor}"
Width="{DynamicResource IconButtonSize}" Height="{DynamicResource IconButtonSize}"
HorizontalAlignment="Right" VerticalAlignment="Center"
Margin="0"
FontFamily="Segoe MDL2 Assets"
Foreground="{DynamicResource MainForegroundColor}"
FontSize="{DynamicResource CloseIconFontSize}"
ToolTip="Minimize"
AutomationProperties.Name="Minimize"
Name="WPFMinimizeButton" />
<Button
BorderThickness="0"
BorderBrush="Transparent"
Background="{DynamicResource MainBackgroundColor}"
Width="{DynamicResource IconButtonSize}" Height="{DynamicResource IconButtonSize}"
HorizontalAlignment="Right" VerticalAlignment="Center"
Margin="0,0,0,0"
FontFamily="Segoe MDL2 Assets"
Foreground="{DynamicResource MainForegroundColor}"
FontSize="{DynamicResource CloseIconFontSize}"
Name="WPFMaximizeButton">
<Button.Style>
<Style TargetType="Button" BasedOn="{StaticResource HoverButtonStyle}">
<Setter Property="Content" Value="&#xE922;"/>
<Setter Property="ToolTip" Value="Maximize"/>
<Setter Property="AutomationProperties.Name" Value="Maximize"/>
<Style.Triggers>
<DataTrigger Binding="{Binding WindowState, RelativeSource={RelativeSource AncestorType={x:Type Window}}}" Value="Maximized">
<Setter Property="Content" Value="&#xE923;"/>
<Setter Property="ToolTip" Value="Restore"/>
<Setter Property="AutomationProperties.Name" Value="Restore"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
<Button
Content="&#xD7;" BorderThickness="0"
BorderBrush="Transparent"
Background="{DynamicResource MainBackgroundColor}"
Width="{DynamicResource IconButtonSize}" Height="{DynamicResource IconButtonSize}"
HorizontalAlignment="Right" VerticalAlignment="Top"
Margin="0,0,0,0"
FontFamily="{DynamicResource FontFamily}"
Foreground="{DynamicResource MainForegroundColor}" FontSize="{DynamicResource CloseIconFontSize}" Name="WPFCloseButton" />
Content="&#xE8BB;"
Style="{StaticResource HoverButtonStyle}"
BorderThickness="0"
BorderBrush="Transparent"
Background="{DynamicResource MainBackgroundColor}"
Width="{DynamicResource IconButtonSize}" Height="{DynamicResource IconButtonSize}"
HorizontalAlignment="Right" VerticalAlignment="Center"
Margin="0"
FontFamily="Segoe MDL2 Assets"
Foreground="{DynamicResource MainForegroundColor}"
FontSize="{DynamicResource CloseIconFontSize}"
ToolTip="Close"
AutomationProperties.Name="Close"
Name="WPFCloseButton" />
</StackPanel>
</Grid>
</Grid>
@@ -1198,8 +1308,33 @@
<TabControl Name="WPFTabNav" Background="Transparent" Width="Auto" Height="Auto" BorderBrush="Transparent" BorderThickness="0" Grid.Row="2" Grid.Column="0" Padding="-1">
<TabItem Header="Install" Visibility="Collapsed" Name="WPFTab1">
<Grid Background="Transparent" >
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid Grid.Row="0" Grid.Column="0" Margin="{DynamicResource TabContentMargin}">
<!-- Quick Category Search Chips -->
<WrapPanel Grid.Row="0" Orientation="Horizontal" Margin="5,5,5,5" Name="WPFSearchChips">
<TextBlock Text="Filters"
FontSize="{DynamicResource HeaderFontSize}"
FontFamily="{DynamicResource HeaderFontFamily}"
Foreground="{DynamicResource LabelboxForegroundColor}"
Background="Transparent"
VerticalAlignment="Center"
Margin="15,0,8,0"/>
<Button Name="WPFSearchChipAll" Content="All" Style="{StaticResource FilterChipStyle}"/>
<Button Name="WPFSearchChipBrowsers" Content="Browsers" Style="{StaticResource FilterChipStyle}"/>
<Button Name="WPFSearchChipCommunications" Content="Communications" Style="{StaticResource FilterChipStyle}"/>
<Button Name="WPFSearchChipDevelopment" Content="Development" Style="{StaticResource FilterChipStyle}"/>
<Button Name="WPFSearchChipGames" Content="Games" Style="{StaticResource FilterChipStyle}"/>
<Button Name="WPFSearchChipMicrosoftTools" Content="Microsoft Tools" Style="{StaticResource FilterChipStyle}"/>
<Button Name="WPFSearchChipMultimediaTools" Content="Multimedia Tools" Style="{StaticResource FilterChipStyle}"/>
<Button Name="WPFSearchChipProTools" Content="Pro Tools" Style="{StaticResource FilterChipStyle}"/>
<Button Name="WPFSearchChipSelfhostedTools" Content="Selfhosted Tools" Style="{StaticResource FilterChipStyle}"/>
<Button Name="WPFSearchChipUtilities" Content="Utilities" Style="{StaticResource FilterChipStyle}"/>
</WrapPanel>
<Grid Grid.Row="1" Margin="{DynamicResource TabContentMargin}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
@@ -1231,13 +1366,14 @@
<StackPanel Background="{DynamicResource MainBackgroundColor}" Orientation="Vertical" Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" Margin="5">
<Label Content="Recommended Selections:" FontSize="{DynamicResource FontSize}" VerticalAlignment="Center" Margin="2"/>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left" Margin="0,2,0,0">
<WrapPanel Orientation="Horizontal" HorizontalAlignment="Left" Margin="0,2,0,0">
<Button Name="WPFstandard" Content=" Standard " Margin="2" Width="{DynamicResource ButtonWidth}" Height="{DynamicResource ButtonHeight}"/>
<Button Name="WPFminimal" Content=" Minimal " Margin="2" Width="{DynamicResource ButtonWidth}" Height="{DynamicResource ButtonHeight}"/>
<Button Name="WPFAdvanced" Content=" Advanced " Margin="2" Width="{DynamicResource ButtonWidth}" Height="{DynamicResource ButtonHeight}"/>
<Button Name="WPFClearTweaksSelection" Content=" Clear " Margin="2" Width="{DynamicResource ButtonWidth}" Height="{DynamicResource ButtonHeight}"/>
<Button Name="WPFGetInstalledTweaks" Content=" Get Installed Tweaks " Margin="2" Width="{DynamicResource ButtonWidth}" Height="{DynamicResource ButtonHeight}"/>
</StackPanel>
<Button Name="WPFAppxRemoval" Content=" AppX Removal " Margin="2" Width="{DynamicResource ButtonWidth}" Height="{DynamicResource ButtonHeight}"/>
</WrapPanel>
</StackPanel>
<Grid Name="tweakspanel" Grid.Row="1">
@@ -1270,99 +1406,148 @@
</TabItem>
<TabItem Header="Updates" Visibility="Collapsed" Name="WPFTab4">
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled" Margin="{DynamicResource TabContentMargin}">
<Grid Background="Transparent" MaxWidth="{Binding ActualWidth, RelativeSource={RelativeSource AncestorType=ScrollViewer}}">
<Grid Background="Transparent" MaxWidth="1250" HorizontalAlignment="Center">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/> <!-- Row for the 3 columns -->
<RowDefinition Height="Auto"/> <!-- Row for Windows Version -->
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- Three columns container -->
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Row="0" Margin="10,10,10,14">
<TextBlock Text="Windows Update Profiles"
FontSize="24"
FontWeight="Bold"
Foreground="{DynamicResource MainForegroundColor}"/>
<TextBlock Text="Choose how Windows receives updates. Each profile replaces the Windows Update settings managed by WinUtil."
Margin="0,6,0,0"
FontSize="13"
TextWrapping="Wrap"
Foreground="{DynamicResource MainForegroundColor}"/>
</StackPanel>
<!-- Default Settings -->
<Border Grid.Column="0" Style="{StaticResource BorderStyle}">
<StackPanel>
<Button Name="WPFUpdatesdefault"
FontSize="{DynamicResource ConfigTabButtonFontSize}"
Content="Default Settings"
Margin="10,5"
Padding="10"/>
<TextBlock Margin="10"
TextWrapping="Wrap"
Foreground="{DynamicResource MainForegroundColor}">
<Run FontWeight="Bold">Default Windows Update Configuration</Run>
<LineBreak/>
- No modifications to Windows defaults
<LineBreak/>
- Removes any custom update settings
<LineBreak/><LineBreak/>
<Run FontStyle="Italic" FontSize="11">Note: This resets your Windows Update settings to default out of the box settings. It removes ANY policy or customization that has been done to Windows Update.</Run>
</TextBlock>
</StackPanel>
</Border>
<!-- Security Settings -->
<Border Grid.Column="1" Style="{StaticResource BorderStyle}">
<StackPanel>
<UniformGrid Grid.Row="1" Columns="3">
<Border Style="{StaticResource BorderStyle}"
BorderBrush="{DynamicResource ProgressBarForegroundColor}"
BorderThickness="2"
Padding="16"
MinHeight="300">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Margin="0,0,0,14">
<TextBlock Text="Recommended"
FontSize="20"
FontWeight="Bold"
Foreground="{DynamicResource MainForegroundColor}"/>
<TextBlock Text="Balanced security and stability"
Margin="0,4,0,0"
FontSize="13"
Foreground="{DynamicResource MainForegroundColor}"/>
</StackPanel>
<StackPanel Grid.Row="1">
<TextBlock Text="- Defers feature updates for 365 days" TextWrapping="Wrap" Margin="0,0,0,7" Foreground="{DynamicResource MainForegroundColor}"/>
<TextBlock Text="- Defers quality updates for 4 days" TextWrapping="Wrap" Margin="0,0,0,7" Foreground="{DynamicResource MainForegroundColor}"/>
<TextBlock Text="- Excludes drivers from quality updates" TextWrapping="Wrap" Margin="0,0,0,7" Foreground="{DynamicResource MainForegroundColor}"/>
<TextBlock Text="- Prevents automatic restarts while a user is signed in" TextWrapping="Wrap" Margin="0,0,0,12" Foreground="{DynamicResource MainForegroundColor}"/>
<TextBlock Text="Available on Windows Pro, Enterprise, and Education editions."
FontSize="11"
FontStyle="Italic"
TextWrapping="Wrap"
Foreground="{DynamicResource MainForegroundColor}"/>
</StackPanel>
<Button Name="WPFUpdatessecurity"
Grid.Row="2"
Content="Apply Recommended"
FontSize="{DynamicResource ConfigTabButtonFontSize}"
Content="Security Settings"
Margin="10,5"
Margin="0,16,0,0"
Padding="10"/>
<TextBlock Margin="10"
TextWrapping="Wrap"
Foreground="{DynamicResource MainForegroundColor}">
<Run FontWeight="Bold">Balanced Security Configuration</Run>
<LineBreak/>
- Feature updates delayed by 365 days
<LineBreak/>
- Security updates installed after 4 days
<LineBreak/>
- Prevents Windows Update from installing drivers
<LineBreak/><LineBreak/>
<Run FontWeight="SemiBold">Feature Updates:</Run> New features and potential bugs
<LineBreak/>
<Run FontWeight="SemiBold">Security Updates:</Run> Critical security patches
<LineBreak/><LineBreak/>
<Run FontStyle="Italic" FontSize="11">Note: This only applies to Pro systems that can use group policy.</Run>
</TextBlock>
</StackPanel>
</Grid>
</Border>
<!-- Disable Updates -->
<Border Grid.Column="2" Style="{StaticResource BorderStyle}">
<StackPanel>
<Border Style="{StaticResource BorderStyle}" Padding="16" MinHeight="300">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Margin="0,0,0,14">
<TextBlock Text="Windows Default"
FontSize="20"
FontWeight="Bold"
Foreground="{DynamicResource MainForegroundColor}"/>
<TextBlock Text="Return control to Windows"
Margin="0,4,0,0"
FontSize="13"
Foreground="{DynamicResource MainForegroundColor}"/>
</StackPanel>
<StackPanel Grid.Row="1">
<TextBlock Text="- Removes Windows Update policies applied by WinUtil" TextWrapping="Wrap" Margin="0,0,0,7" Foreground="{DynamicResource MainForegroundColor}"/>
<TextBlock Text="- Restores update service startup settings" TextWrapping="Wrap" Margin="0,0,0,7" Foreground="{DynamicResource MainForegroundColor}"/>
<TextBlock Text="- Re-enables update scheduled tasks" TextWrapping="Wrap" Margin="0,0,0,12" Foreground="{DynamicResource MainForegroundColor}"/>
<TextBlock Text="Use this to undo the Recommended or Disable profile."
FontSize="11"
FontStyle="Italic"
TextWrapping="Wrap"
Foreground="{DynamicResource MainForegroundColor}"/>
</StackPanel>
<Button Name="WPFUpdatesdefault"
Grid.Row="2"
Content="Restore Defaults"
FontSize="{DynamicResource ConfigTabButtonFontSize}"
Margin="0,16,0,0"
Padding="10"/>
</Grid>
</Border>
<Border Style="{StaticResource BorderStyle}" Padding="16" MinHeight="300">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Margin="0,0,0,14">
<TextBlock Text="Disable Updates"
FontSize="20"
FontWeight="Bold"
Foreground="Red"/>
<TextBlock Text="Advanced use only"
Margin="0,4,0,0"
FontSize="13"
FontWeight="SemiBold"
Foreground="Red"/>
</StackPanel>
<StackPanel Grid.Row="1">
<TextBlock Text="- Disables automatic update policy" TextWrapping="Wrap" Margin="0,0,0,7" Foreground="{DynamicResource MainForegroundColor}"/>
<TextBlock Text="- Stops update services and scheduled tasks" TextWrapping="Wrap" Margin="0,0,0,7" Foreground="{DynamicResource MainForegroundColor}"/>
<TextBlock Text="- Clears downloaded update files" TextWrapping="Wrap" Margin="0,0,0,12" Foreground="{DynamicResource MainForegroundColor}"/>
<TextBlock Text="Security updates will not be installed while this profile is active."
FontSize="11"
FontStyle="Italic"
TextWrapping="Wrap"
Foreground="Red"/>
</StackPanel>
<Button Name="WPFUpdatesdisable"
Grid.Row="2"
Content="Disable Updates"
FontSize="{DynamicResource ConfigTabButtonFontSize}"
Content="Disable All Updates"
Foreground="Red"
Margin="10,5"
Margin="0,16,0,0"
Padding="10"/>
<TextBlock Margin="10"
TextWrapping="Wrap"
Foreground="{DynamicResource MainForegroundColor}">
<Run FontWeight="Bold" Foreground="Red">!! Not Recommended !!</Run>
<LineBreak/>
- Disables ALL Windows Updates
<LineBreak/>
- Increases security risks
<LineBreak/>
- Only use for isolated systems
<LineBreak/><LineBreak/>
<Run FontStyle="Italic" FontSize="11">Warning: Your system will be vulnerable without security updates.</Run>
</TextBlock>
</StackPanel>
</Grid>
</Border>
</Grid>
</UniformGrid>
<!-- Future Implementation: Add Windows Version to updates panel -->
<Grid Name="updatespanel" Grid.Row="1" Background="Transparent">
</Grid>
<Border Grid.Row="2" Style="{StaticResource BorderStyle}" Margin="8,14,8,8" Padding="12">
<TextBlock Text="Changes apply system-wide. Restart Windows after switching profiles. Use Restore Defaults to undo WinUtil update policies."
TextWrapping="Wrap"
HorizontalAlignment="Center"
Foreground="{DynamicResource MainForegroundColor}"/>
</Border>
</Grid>
</ScrollViewer>
</TabItem>
@@ -1497,7 +1682,7 @@
Foreground="{DynamicResource MainForegroundColor}"
IsChecked="False"
Margin="0,8,0,0"
ToolTip="Exports all drivers from this machine and injects them into install.wim and boot.wim. Recommended for systems with unsupported NVMe or network controllers."/>
ToolTip="Stages boot-storage drivers for Setup and adds all exported drivers to the selected install.wim edition in one DISM pass."/>
</StackPanel>
<!-- Verification results panel -->
@@ -1707,8 +1892,9 @@
<Border Grid.Row="2" Style="{StaticResource BorderStyle}" Margin="5,15,5,5">
<StackPanel Background="{DynamicResource MainBackgroundColor}" Orientation="Horizontal" HorizontalAlignment="Left">
<TextBlock Padding="10" TextWrapping="Wrap" Foreground="{DynamicResource MainForegroundColor}">
Note: Select the pre-installed Windows AppX packages you wish to remove and click 'Remove Selected'.
<LineBreak/>These packages are removed for the current user and all new user profiles.
Note: Select the Windows AppX packages you wish to install or remove.
<LineBreak/>Install Selected registers a local manifest when available, then falls back to the Microsoft Store.
<LineBreak/>Remove Selected removes packages for the current user and all new user profiles.
</TextBlock>
</StackPanel>
</Border>
@@ -1717,11 +1903,21 @@
<Border Grid.Row="1" Background="{DynamicResource MainBackgroundColor}" BorderBrush="{DynamicResource BorderColor}" BorderThickness="1" CornerRadius="5" HorizontalAlignment="Stretch" Padding="10">
<WrapPanel Orientation="Horizontal" HorizontalAlignment="Left" VerticalAlignment="Center">
<Button Name="WPFBackToTweaks" Content="Back to Tweaks" Margin="5" Width="{DynamicResource ButtonWidth}" Height="{DynamicResource ButtonHeight}"/>
<Button Name="WPFInstallSelectedAppx" Content="Install Selected" Margin="5" Width="{DynamicResource ButtonWidth}" Height="{DynamicResource ButtonHeight}"/>
<Button Name="WPFRemoveSelectedAppx" Content="Remove Selected" Margin="5" Width="{DynamicResource ButtonWidth}" Height="{DynamicResource ButtonHeight}"/>
</WrapPanel>
</Border>
</Grid>
</TabItem>
</TabControl>
<!-- Window-level progress indicator - visible regardless of active tab -->
<Border Name="WPFTweaksProgressBar" Grid.Row="3" Background="{DynamicResource MainBackgroundColor}" Visibility="Collapsed" Padding="10,6">
<StackPanel Orientation="Vertical">
<TextBlock Name="WPFTweaksProgressLabel" Text="" Foreground="{DynamicResource MainForegroundColor}" FontSize="13" Background="Transparent" Margin="0,0,0,4"/>
<ProgressBar Name="WPFTweaksProgressValue" Height="6" Minimum="0" Maximum="100" Value="0" Style="{StaticResource RoundedProgressBarStyle}"/>
</StackPanel>
</Border>
</Grid>
</Window>