Skip to content
Draft
Show file tree
Hide file tree
Changes from 56 commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
250f2ee
Add a job layer for long running work
MyDrift-user Aug 5, 2026
3867f4a
Move the remaining app and feature workflows onto the job layer
MyDrift-user Aug 6, 2026
94ed3ea
Run the WinUtil interface on its own thread
MyDrift-user Aug 6, 2026
c60e5c7
Move every long workflow onto the job layer
MyDrift-user Aug 6, 2026
e740c84
Log from every WinUtil thread into one session file
MyDrift-user Aug 6, 2026
d90a90d
Document the threading model and the job layer
MyDrift-user Aug 6, 2026
2488adb
Stop Invoke-WPFUIThread leaking the body's output to its caller
MyDrift-user Aug 6, 2026
074facc
Put every button that changes the system on the job layer
MyDrift-user Aug 6, 2026
5069853
Document button routing and the job banner
MyDrift-user Aug 6, 2026
5b75fee
Read function bodies from the FunctionInfo instead of the function pr…
MyDrift-user Aug 6, 2026
997cdbf
Render the taskbar overlays after first paint
MyDrift-user Aug 6, 2026
5b1ac05
Make a failed package fail the job
MyDrift-user Aug 6, 2026
ccf8986
Time every pipeline step and report the slowest ones
MyDrift-user Aug 6, 2026
a73fa4c
Render the taskbar overlays on the main thread's runspace
MyDrift-user Aug 6, 2026
6bbc2b3
Cut startup to first interaction roughly in half
MyDrift-user Aug 6, 2026
276487b
Warm the unopened tabs while the interface is idle
MyDrift-user Aug 6, 2026
1464925
Report failures with the context needed to act on them
MyDrift-user Aug 6, 2026
60f73fe
Wire the Install tab controls where they are created
MyDrift-user Aug 6, 2026
4c1c10a
Stop losing warnings and non-terminating errors from job bodies
MyDrift-user Aug 6, 2026
2e83b32
Drive winget through Microsoft.WinGet.Client for real progress
MyDrift-user Aug 6, 2026
8717d6c
Show the install phase as running instead of finished
MyDrift-user Aug 6, 2026
8f52689
Say what a winget failure actually was
MyDrift-user Aug 6, 2026
06e75cf
Keep the run position in the progress text during a package
MyDrift-user Aug 6, 2026
299508d
Pulse the progress bar in place instead of filling it
MyDrift-user Aug 6, 2026
924ab6f
perf: cut Install tab build and keep the interface answering during w…
MyDrift-user Aug 7, 2026
70dc7a7
xaml: drop layout elements and styles that do nothing
MyDrift-user Aug 7, 2026
0174862
fix: bring the last workflows into the job layer
MyDrift-user Aug 8, 2026
6b51a6c
fix: keep the runspace handle out of the console and the pipeline
MyDrift-user Aug 8, 2026
d636628
fix: choco parity with winget, and prompts that cannot hang or assume…
MyDrift-user Aug 8, 2026
9c4021f
feat: headless runs report and finish on their own
MyDrift-user Aug 8, 2026
8f5b0fa
perf: stop the interface stalling while the app list loads
MyDrift-user Aug 8, 2026
84ec372
fix: build the other tabs before finishing the app list
MyDrift-user Aug 8, 2026
deb206e
fix: ask before closing over running work, and shut down in order
MyDrift-user Aug 8, 2026
3883460
fix: let a running job finish in the console after the window is closed
MyDrift-user Aug 8, 2026
aa18d9c
feat: pause button, and progress that rewrites its line
MyDrift-user Aug 8, 2026
7a6d00d
feat: stop button for the running action
MyDrift-user Aug 8, 2026
ac00fba
fix: use a stop glyph that does not look like a missing one
MyDrift-user Aug 8, 2026
1a1cc28
fix: icon buttons render in the icon font, not a fallback
MyDrift-user Aug 8, 2026
88245f4
fix: the font scaling slider goes where it is clicked
MyDrift-user Aug 8, 2026
464ef3e
fix: draw the logo at the size it is asked for, and give the window i…
MyDrift-user Aug 8, 2026
c2a19ed
fix: give the nav logo its square box back
MyDrift-user Aug 8, 2026
3c0cffa
Merge remote-tracking branch 'upstream/main' into feat/async-architec…
MyDrift-user Aug 11, 2026
07cf03e
refactor: one background queue, one UI-alive check, drop the stall tr…
MyDrift-user Aug 17, 2026
0b3f1ef
docs: bring the agent rules in line with the job layer
MyDrift-user Aug 17, 2026
8cffa53
refactor: trim the package work back to what the pipeline needs
MyDrift-user Aug 17, 2026
150fe8c
test: drop the tests that assert on source text
MyDrift-user Aug 17, 2026
20f7215
Merge remote-tracking branch 'upstream/main' into feat/async-job-layer
MyDrift-user Aug 17, 2026
26f2de4
fix: make the active job slot safe to claim and release
MyDrift-user Aug 17, 2026
bc915dc
refactor: drop the WinGet client module from this branch
MyDrift-user Aug 17, 2026
34d95b5
docs: follow the Write-WinUtilJobProgress rename in the architecture …
MyDrift-user Aug 18, 2026
bfd0a24
fix: address the review findings on the job layer
MyDrift-user Aug 18, 2026
59a6122
fix: address the remaining review findings
MyDrift-user Aug 18, 2026
197c9e7
fix: address the minor and nitpick review findings
MyDrift-user Aug 18, 2026
df5f6bb
fix: startup crash and icons that only appeared once the list was drawn
MyDrift-user Aug 18, 2026
3fe7d52
fix: address the second review pass
MyDrift-user Aug 18, 2026
c15e31e
fix: treat chkdsk's own exit codes as the outcomes they are
MyDrift-user Aug 19, 2026
fc49735
Merge remote-tracking branch 'upstream/main' into feat/async-job-layer
MyDrift-user Aug 19, 2026
9435ad4
fix: address the Codex review findings on the job layer
MyDrift-user Aug 19, 2026
5c2f172
Merge remote-tracking branch 'upstream/main' into feat/async-job-layer
MyDrift-user Aug 20, 2026
98f2bfe
fix: stop the tab warmup dying on a sync that is still growing
MyDrift-user Aug 20, 2026
3b0ddda
fix: shrink the pause and stop buttons and take them away when idle
MyDrift-user Aug 20, 2026
8769d07
fix: colour the progress bar by how the run ended
MyDrift-user Aug 20, 2026
3b32160
fix: stop rendering the app navigation twice, which left Install dead
MyDrift-user Aug 20, 2026
0c71f80
fix: uninstall apps that belong to the signed in user
MyDrift-user Aug 20, 2026
ef9eb0c
style: keep the ISO variable names the file already used
MyDrift-user Aug 20, 2026
9393643
refactor: take the icon cache out of the job layer change
MyDrift-user Aug 20, 2026
6ae8431
refactor: take pause and stop out of the job layer change
MyDrift-user Aug 20, 2026
19c33db
refactor: drop changes the job layer does not need
MyDrift-user Aug 20, 2026
2be53be
refactor: cut comments and wrappers that were not earning their place
MyDrift-user Aug 21, 2026
d8b8518
refactor: comments describe the code, not how it got here
MyDrift-user Aug 21, 2026
8ab39b9
refactor: inline the winget error message at its only call site
MyDrift-user Aug 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,12 @@ When the user corrects an agent approach, add or tighten one concrete rule here
- Import Pester 5.8.0 before running tests so `Invoke-Pester -Output Detailed -CI` does not resolve to Windows' inbox Pester 3.4.0.
- Keep package install/uninstall process launches simple unless explicitly requested; do not add a separate stdout/stderr process logging helper for winget or Chocolatey.
- 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.
- Keep UI helpers such as `Invoke-WPFUIThread` and `Set-WinUtilTweaksProgressIndicator` safe to call without a window; the `-Preset` and `-Config` paths run the workflows before the form is created and before PresentationCore is loaded.
- Keep UI helpers such as `Invoke-WPFUIThread` and `Write-WinUtilJobProgress` safe to call without a window; the `-Preset` and `-Config` paths run the workflows before the form is created and before PresentationCore is loaded. Ask `Test-WinUtilUIAlive` rather than writing the `$sync.Form` / dispatcher / `HasShutdownStarted` check out by hand.
- Put long operations on the job layer with `Start-WinUtilJob` and report from them with `Write-WinUtilJobProgress`; a job body must not set the busy flag, print its own banner, or carry its own try/catch/finally around the interface. `Invoke-WPFRunspace` directly is for fire-and-forget work that is not a job.
- Drain interface work that nobody is waiting for through `Start-WinUtilBackgroundQueue`; do not hand-roll another dequeue-and-re-post pump.
- Values a posted scriptblock needs travel as the dispatcher's argument or through `-Parameters`, never captured from the caller: a plain block resolves them when the dispatcher gets to it, and `GetNewClosure` binds command lookup to a copied scope. For the same reason, prefer a compiled `[action]` over `Invoke-WPFUIThread -Async` on hot re-posting paths, which marshals its body as text and recompiles it per post.
- Diagnostic scaffolding does not ship. Measure with it, then delete it.
- Have each Pester file load the assemblies and dot-source the functions it needs; several passed only because an earlier file in alphabetical order happened to load them.
- 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.
Expand Down
9 changes: 7 additions & 2 deletions config/tweaks.json
Original file line number Diff line number Diff line change
Expand Up @@ -1086,8 +1086,13 @@
"panel": "1",
"InvokeScript": [
"
Remove-Item -Path \"$Env:Temp\\*\" -Recurse -Force
Remove-Item -Path \"$Env:SystemRoot\\Temp\\*\" -Recurse -Force
$before = @(Get-ChildItem -Path \"$Env:Temp\", \"$Env:SystemRoot\\Temp\" -Force -ErrorAction SilentlyContinue).Count
# A temp folder always holds files something has open, including this run's own. Those are
# skipped rather than reported, or every cleanup would look like a failed one.
Remove-Item -Path \"$Env:Temp\\*\" -Recurse -Force -ErrorAction SilentlyContinue
Remove-Item -Path \"$Env:SystemRoot\\Temp\\*\" -Recurse -Force -ErrorAction SilentlyContinue
$after = @(Get-ChildItem -Path \"$Env:Temp\", \"$Env:SystemRoot\\Temp\" -Force -ErrorAction SilentlyContinue).Count
Write-Host \"Removed $($before - $after) temporary item(s); $after still in use.\"
"
],
"link": "https://winutil.christitus.com/code-reference/tweaks/essential-tweaks/deletetempfiles"
Expand Down
120 changes: 86 additions & 34 deletions docs/src/content/docs/code-reference/architecture.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -79,14 +79,15 @@ winutil/
**Why**: Makes distribution easier (single file) and improves load time.

#### 2. scripts/main.ps1
**Purpose**: Entry point that initializes the GUI and event system.
**Purpose**: Entry point that manages the run.

**Responsibilities**:
- Load XAML and create WPF window
- Initialize form elements
- Set up event handlers
- Load configurations
- Display the GUI
- Run the headless `-Preset` and `-Config` paths
- Start the interface on a dedicated STA runspace and wait for it
- Report anything the interface thread failed with, then clean up

The interface itself lives in `Start-WinUtilUserInterface`, not here. See [Threading Model](#threading-model).

#### 3. functions/public/
**Purpose**: User-facing functions that implement main features.
Expand Down Expand Up @@ -390,23 +391,68 @@ Update UI
- `service`: Services to change
- `OriginalValue/State`: For undo functionality

## PowerShell Runspace
## Threading Model

WinUtil runs on three kinds of thread, and each has one job:

| Thread | Runspace | Responsibility |
| --- | --- | --- |
| Main | The one the script started in | Start the interface, wait for it, surface its errors, clean up |
| Interface | `$sync.UIRunspace`, a dedicated STA runspace | Own the window. Paint and dispatch, nothing else |
| Workers | `$sync.runspace`, a shared pool | Run everything long: installs, tweaks, features, AppX, Win11 Creator |

Winutil uses PowerShell runspaces for the GUI to remain responsive:
All three are created from the same starting point, `New-WinUtilSessionState`, which carries
`$sync`, the compiled script's globals, and every WinUtil function. That is what lets any
thread call any helper without the caller injecting function definitions.

```powershell
# Create runspace
$sync.runspace = [runspacefactory]::CreateRunspace()
$sync.runspace.Open()
$sync.runspace.SessionStateProxy.SetVariable("sync", $sync)
# Run code in background
$powershell = [powershell]::Create().AddScript($scriptblock)
$powershell.Runspace = $sync.runspace
$handle = $powershell.BeginInvoke()
# main.ps1 - the interface gets its own thread
$sync.UIRunspace = [runspacefactory]::CreateRunspace($Host, (New-WinUtilSessionState))
$sync.UIRunspace.ApartmentState = "STA"
$sync.UIRunspace.Open()
$uiShell = [powershell]::Create()
$uiShell.Runspace = $sync.UIRunspace
[void]$uiShell.AddScript({ Start-WinUtilUserInterface })
$uiHandle = $uiShell.BeginInvoke()
$uiHandle.AsyncWaitHandle.WaitOne()
```

**Why**: Prevents UI freezing during long-running operations.
**Why**: the window never blocks on work, and a failure on the interface thread is reported
instead of disappearing.

## Long-Running Work

Every long action goes through `Start-WinUtilJob`, which owns everything a running operation
needs: refusing to start while another job runs, the busy flag (`$sync.ActiveJob`), the progress
bar and taskbar item, the boxed console banner, a start/finish/failure line in the log, and
restoring the interface in a `finally` whatever happens.

`Invoke-WPFButton` decides what counts as a long action. Anything that changes the system gets a
job; anything that only changes what the interface is showing runs on the interface thread.
That single classification is why no workflow arranges its own progress, banner or busy state.

```powershell
Start-WinUtilJob -Name "Features" -Description "Installing Windows Features" -Parameters @{
Features = @($sync.selectedFeatures)
} -ScriptBlock {
param($Features)
$total = @($Features).Count
$completed = 0
foreach ($feature in $Features) {
$completed++
Step-WinUtilJob -Status "Installing $feature ($completed/$total)" -Percent ([int](($completed / $total) * 100))
Invoke-WinUtilFeatureInstall $feature
}
}
```

The body only has to do the work and call `Step-WinUtilJob`. Anything it throws is
caught, logged, and shown on the taskbar as a failure.

**Values, not closures**: the body is rebuilt inside the worker from its text, so it receives
what it needs through `-Parameters` rather than capturing the caller's variables.

## WPF Event Handling

Expand Down Expand Up @@ -452,23 +498,21 @@ if (!(Get-Command choco -ErrorAction SilentlyContinue)) {
choco install $app.choco -y
```

## Error Handling
## Error Handling And Logging

Winutil uses PowerShell error handling:
A job body does not need its own error handling. `Start-WinUtilJob` catches whatever the body
throws, logs it, and marks the run as failed on the taskbar, so a failure can never leave the
interface stuck busy. Only catch inside a body when you have something specific to do first,
such as cleaning up a mounted image, and then rethrow.

```powershell
try {
# Attempt operation
Invoke-SomeOperation
}
catch {
Write-Host "Error: $_" -ForegroundColor Red
# Log error
Add-Content -Path $logfile -Value "ERROR: $_"
}
Write-WinUtilLog -Level "ERROR" -Component "Install" -Message "winget install failed: $($_.Exception.Message)"
```

**Logging**: Errors and operations are logged for debugging.
Entries go to `%LocalAppData%\winutil\logs\winutil_<timestamp>.log` from every thread, guarded
by a named mutex. The console transcript is a separate file in the same directory, because
`Start-Transcript` keeps its file open and only records the runspace it was started on - so
anything a worker logged would otherwise never reach disk.

## Configuration Loading

Expand All @@ -486,15 +530,23 @@ $sync.configs.features = Get-Content "config/feature.json" | ConvertFrom-Json

## UI Update Pattern

UI updates must happen on the UI thread:
Controls may only be touched from the thread that owns the window, so background work reaches
them through `Invoke-WPFUIThread`:

```powershell
$sync.form.Dispatcher.Invoke([action]{
$sync.WPFStatusLabel.Content = "Installing..."
}, "Normal")
Invoke-WPFUIThread -Parameters @{ Count = $installed.Count } -ScriptBlock {
param($Count)
$sync.WPFselectedAppsButton.Content = "Selected Apps: $Count"
}
```

**Why**: WPF requires UI updates on the main thread.
Add `-Async` to post the update instead of waiting for it. `Step-WinUtilJob` and the
Win11 Creator status log use that so a per-item update never stalls the worker.

**Values, not closures**: the body is rebuilt inside the interface runspace, so it takes what it
needs through `-Parameters`. Handing over a scriptblock from a worker instead would keep that
worker's session state, which loses the caller's variables on an async post and costs roughly
twenty times as much per command - enough to turn a checkbox refresh into a visible freeze.

## Adding New Features

Expand Down
37 changes: 36 additions & 1 deletion functions/private/Close-WinUtilRunspacePool.ps1
Original file line number Diff line number Diff line change
@@ -1,8 +1,39 @@
function Close-WinUtilRunspacePool {
<#
.SYNOPSIS
Stops anything still running and closes the worker pool

.DESCRIPTION
Closing the pool with work still in it is what produced an unhandled
InvalidRunspaceStateException: a queued instance starts on a runspace that is already
closing, throws on a thread pool thread, and takes the process down. Whatever is in
flight is therefore asked to stop, and waited for, before the pool is closed.
#>
param(
[int]$StopTimeoutSeconds = 15,

# Replacing a pool that is no longer usable, rather than shutting WinUtil down. The
# difference matters: ShuttingDown is what makes the job layer refuse work, and nothing
# resets it, so recycling through the shutdown path would reject every later action for
# the rest of the session.
[switch]$Recycle
)

if ($null -eq $sync -or -not $sync.ContainsKey("runspace") -or $null -eq $sync.runspace) {
return
}

# Set before stopping, so nothing that is winding down queues fresh work behind us
if (-not $Recycle) {
$sync.ShuttingDown = $true
}

try {
Stop-WinUtilActiveWork -TimeoutSeconds $StopTimeoutSeconds | Out-Null
} catch {
Write-WinUtilLog -Level "WARN" -Component "UI" -Message "Could not stop running work cleanly: $($_.Exception.Message)"
}

try {
if ($sync.runspace.RunspacePoolStateInfo.State -notin @(
[System.Management.Automation.Runspaces.RunspacePoolState]::Closed,
Expand All @@ -11,8 +42,12 @@ function Close-WinUtilRunspacePool {
)) {
$sync.runspace.Close()
}
} catch {
# A pool that will not close cleanly must not stop the window from closing
Write-WinUtilLog -Level "WARN" -Component "UI" -Message "Worker pool did not close cleanly: $($_.Exception.Message)"
} finally {
$sync.runspace.Dispose()
try { $sync.runspace.Dispose() } catch { }
$sync.Remove("runspace")
if ($sync.ActiveShells) { $sync.ActiveShells.Clear() }
}
}
50 changes: 50 additions & 0 deletions functions/private/Complete-WinUtilPackageRun.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
function Complete-WinUtilPackageRun {
<#
.SYNOPSIS
Reports what a package run actually did and fails the job if anything did not work

.DESCRIPTION
Package managers report failure through an exit code, which is easy to walk past.
Without this the job layer would show a green checkmark for a run in which nothing
installed. Throwing here is what turns a failed package into a failed job.

.PARAMETER Action
Install or Uninstall, used in the summary text.

.PARAMETER Results
The result objects produced by Install-WinUtilProgramWinget and
Install-WinUtilProgramChoco.
#>
param(
[Parameter(Mandatory)]
[string]$Action,

[object[]]$Results = @()
)

$succeeded = @($Results | Where-Object { $_.Outcome -eq "Succeeded" })
$skipped = @($Results | Where-Object { $_.Outcome -eq "Skipped" })
$failed = @($Results | Where-Object { $_.Outcome -eq "Failed" })

$summary = "$($succeeded.Count) succeeded, $($skipped.Count) skipped, $($failed.Count) failed"
Write-WinUtilLog -Component "Package" -Message "$Action summary: $summary"
Write-Host "$Action summary: $summary"

foreach ($result in $skipped) {
Write-Host " skipped $($result.Package) - $($result.Detail)"
}
foreach ($result in $failed) {
Write-Host " failed $($result.Package) - $($result.Detail)" -ForegroundColor Red
}

if ($failed.Count -gt 0) {
$names = ($failed | ForEach-Object { $_.Package }) -join ', '
$reasons = @($failed | ForEach-Object { $_.Detail } | Sort-Object -Unique)

# One shared reason is worth repeating; several would bury it, and they are listed above
if ($reasons.Count -eq 1) {
throw "$($failed.Count) of $($Results.Count) package(s) failed: $names. $($reasons[0])"
}
throw "$($failed.Count) of $($Results.Count) package(s) failed: $names. See the lines above for each reason."
}
}
60 changes: 60 additions & 0 deletions functions/private/Get-WinUtilAppEntryHandlers.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
function Get-WinUtilAppEntryHandlers {
<#
.SYNOPSIS
The event handlers shared by every app entry on the Install tab

.DESCRIPTION
A scriptblock literal inside a loop is a new scriptblock every time round, and
building six of them per app is the single largest cost of drawing the app list:
measured at 2.13 ms per entry against 0.62 ms when they are made once and reused.

None of them close over anything per entry. They read the sender through $this, so
one instance serves every app.
#>

if ($null -ne $script:WinUtilAppEntryHandlers) {
return $script:WinUtilAppEntryHandlers
}

$script:WinUtilAppEntryHandlers = @{
BorderClick = {
# Resolve through $sync because the border's child is a layout Grid for FOSS entries
$childCheckbox = $sync.$($this.Tag)
$childCheckbox.IsChecked = -not $childCheckbox.IsChecked
}
MouseEnter = {
if (($sync.$($this.Tag).IsChecked) -eq $false) {
$this.SetResourceReference([Windows.Controls.Control]::BackgroundProperty, "AppInstallHighlightedColor")
}
}
MouseLeave = {
if (($sync.$($this.Tag).IsChecked) -eq $false) {
$this.SetResourceReference([Windows.Controls.Control]::BackgroundProperty, "AppInstallUnselectedColor")
}
}
RightClick = {
# Store the selected app in a global variable so it can be used in the popup
$sync.appPopupSelectedApp = $this.Tag
# Set the popup position to the current mouse position
$sync.appPopup.PlacementTarget = $this
$sync.appPopup.IsOpen = $true
}
# The checkbox sits inside the entry layout Grid, so the border is one level further up
Checked = {
Invoke-WPFSelectedCheckboxesUpdate -type "Add" -checkboxName $this.Tag
$borderElement = $this.Parent.Parent
$borderElement.SetResourceReference([Windows.Controls.Control]::BackgroundProperty, "AppInstallSelectedColor")
}
Unchecked = {
Invoke-WPFSelectedCheckboxesUpdate -type "Remove" -checkboxName $this.Tag
$borderElement = $this.Parent.Parent
$borderElement.SetResourceReference([Windows.Controls.Control]::BackgroundProperty, "AppInstallUnselectedColor")
}
ImageFailed = {
$this.Visibility = "Collapsed"
$this.Parent.Children[0].Visibility = "Visible"
}
}

return $script:WinUtilAppEntryHandlers
}
5 changes: 2 additions & 3 deletions functions/private/Get-WinUtilSelectedPackages.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,9 @@ function Get-WinUtilSelectedPackages {
[string] $Preference
)

# A single package has no meaningful percentage to show
if ($PackageList.count -eq 1) {
Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -state "Indeterminate" -value 0.01 -overlay "logo" }
} else {
Invoke-WPFUIThread -ScriptBlock { Set-WinUtilTaskbaritem -state "Normal" -value 0.01 -overlay "logo" }
Step-WinUtilJob -State "Indeterminate"
}

$packagesWinget = [System.Collections.ArrayList]::new()
Expand Down
Loading