Skip to content
2 changes: 1 addition & 1 deletion docs/src/content/docs/code-reference/architecture.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ Invoke-WinUtilISOCleanAndReset (optional)
**Modification Safety**:
- All registry changes are documented in a script (reversible)
- Original ISO never modified; only working copy
- Logged to `WinUtil_Win11ISO.log` for debugging
- Logged to a `WinUtil_Win11ISO_*.log` file beside the working directory, so it survives cleanup
- DISM handles image dismount with automatic cleanup on error

### Win11 Creator Registry Tweaks
Expand Down
18 changes: 5 additions & 13 deletions functions/private/Find-AppsByNameOrDescription.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -91,42 +91,34 @@ function Find-AppsByNameOrDescription {
return
}

# Escape wildcard characters for literal matching
$escapedSearchString = [System.Management.Automation.WildcardPattern]::Escape($SearchString)

# IndexOf with OrdinalIgnoreCase is faster than -like with wildcard escaping
$sync.ItemsControl.Items | ForEach-Object {
# Each item is a StackPanel container with Children[0] = label, Children[1] = WrapPanel
if ($_.Children.Count -ge 2) {
$categoryLabel = $_.Children[0]
$wrapPanel = $_.Children[1]
$categoryHasMatch = $false

$categoryLabel.Visibility = [Windows.Visibility]::Visible

foreach ($appControl in $wrapPanel.Children) {
$appTag = $appControl.Tag
$appEntry = $null

if (-not [string]::IsNullOrWhiteSpace($appTag) -and $sync.configs.applicationsHashtable.ContainsKey($appTag)) {
$appEntry = $sync.configs.applicationsHashtable[$appTag]
}

if ($null -ne $appEntry) {
$categoryMatch = -not $hasCategories -or $activeCategories -contains $appEntry.Category
$textMatch = -not $hasSearch -or
$appEntry.Content -like "*$escapedSearchString*" -or
$appEntry.Description -like "*$escapedSearchString*"
([string]$appEntry.Content).IndexOf($SearchString, [System.StringComparison]::OrdinalIgnoreCase) -ge 0 -or
([string]$appEntry.Description).IndexOf($SearchString, [System.StringComparison]::OrdinalIgnoreCase) -ge 0

if ($categoryMatch -and $textMatch) {
$appControl.Visibility = [Windows.Visibility]::Visible
$categoryHasMatch = $true
}
else {
} else {
$appControl.Visibility = [Windows.Visibility]::Collapsed
}
}
else {
# Hide app if no entry found (data integrity issue)
} else {
$appControl.Visibility = [Windows.Visibility]::Collapsed
}
}
Expand Down
379 changes: 93 additions & 286 deletions functions/private/Find-TweaksByNameOrDescription.ps1

Large diffs are not rendered by default.

62 changes: 26 additions & 36 deletions functions/private/Invoke-WinUtilCurrentSystem.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ Function Invoke-WinUtilCurrentSystem {
)
if ($CheckBox -eq "choco") {
$apps = (choco list | Select-String -Pattern "^\S+").Matches.Value
$sync.configs.applicationsHashtable.GetEnumerator() | ForEach-Object {
$packageId = ($_.Value.choco -split ";")[-1].Trim()
foreach ($app in $sync.configs.applicationsHashtable.GetEnumerator()) {
$packageId = ($app.Value.choco -split ";")[-1].Trim()
if ($packageId -ne "na" -and $packageId -in $apps) {
Write-Output $_.Key
Write-Output $app.Key
}
}
}
Expand All @@ -36,15 +36,15 @@ Function Invoke-WinUtilCurrentSystem {
}
$installedProgramText = $installedProgramOutput -join "`n"

$sync.configs.applicationsHashtable.GetEnumerator() | ForEach-Object {
$packageId = (($_.Value.winget -split ";")[-1] -replace "^msstore:", "").Trim()
foreach ($app in $sync.configs.applicationsHashtable.GetEnumerator()) {
$packageId = (($app.Value.winget -split ";")[-1] -replace "^msstore:", "").Trim()
if ([string]::IsNullOrWhiteSpace($packageId) -or $packageId -eq "na") {
return
continue
}

$packagePattern = "(?im)[^\S\r\n]{2,}$([regex]::Escape($packageId))(?=[^\S\r\n]{2,}|$)"
if ($installedProgramText -match $packagePattern) {
Write-Output $_.Key
Write-Output $app.Key
}
}
}
Expand All @@ -53,27 +53,26 @@ Function Invoke-WinUtilCurrentSystem {

if (!(Test-Path 'HKU:\')) {$null = (New-PSDrive -PSProvider Registry -Name HKU -Root HKEY_USERS)}

$sync.configs.tweaks | Get-Member -MemberType NoteProperty | ForEach-Object {

$Config = $psitem.Name
$entry = $sync.configs.tweaks.$Config
foreach ($prop in $sync.configs.tweaks.PSObject.Properties) {
$Config = $prop.Name
$entry = $prop.Value
$registryKeys = $entry.registry
$serviceKeys = $entry.service
$entryType = $entry.Type

if (($registryKeys -or $serviceKeys) -and $entryType -ne "Combobox") {
$Values = @()
$allMatch = $true

if ($entryType -eq "Toggle") {
if (-not (Get-WinUtilToggleStatus $Config)) {
$values += $False
$allMatch = $false
}
} else {
$registryMatchCount = 0
$registryTotal = 0

Foreach ($tweaks in $registryKeys) {
Foreach ($tweak in $tweaks) {
foreach ($tweaks in $registryKeys) {
foreach ($tweak in $tweaks) {
$registryTotal++
$regstate = $null

Expand All @@ -83,15 +82,9 @@ Function Invoke-WinUtilCurrentSystem {

if ($null -eq $regstate) {
switch ($tweak.DefaultState) {
"true" {
$regstate = $tweak.Value
}
"false" {
$regstate = $tweak.OriginalValue
}
default {
$regstate = $tweak.OriginalValue
}
"true" { $regstate = $tweak.Value }
"false" { $regstate = $tweak.OriginalValue }
default { $regstate = $tweak.OriginalValue }
}
}

Expand All @@ -102,25 +95,22 @@ Function Invoke-WinUtilCurrentSystem {
}

if ($registryTotal -gt 0 -and $registryMatchCount -ne $registryTotal) {
$values += $False
$allMatch = $false
}
}

Foreach ($tweaks in $serviceKeys) {
Foreach ($tweak in $tweaks) {
$Service = Get-Service -Name $tweak.Name

if ($Service) {
$actualValue = $Service.StartType
$expectedValue = $tweak.StartupType
if ($expectedValue -ne $actualValue) {
$values += $False
}
foreach ($tweaks in $serviceKeys) {
foreach ($tweak in $tweaks) {
$Service = Get-Service -Name $tweak.Name -ErrorAction SilentlyContinue
if (-not $Service -or $tweak.StartupType -ne $Service.StartType) {
Comment on lines +104 to +105

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Ignore absent optional services during tweak detection

On Windows editions/builds where a configured tweak service is absent (for example optional services such as CscService or MapsBroker), applying the tweak already only logs a warning and continues via Set-WinUtilService, but this detection path now marks the whole tweak as not applied whenever Get-Service returns nothing. That makes “Get Installed” leave a successfully applied tweak unchecked forever on those systems; keep missing services neutral as before, or align apply/detect semantics so absent services do not block detection.

Useful? React with 👍 / 👎.

$allMatch = $false
break
}
}
if (-not $allMatch) { break }
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

if ($values -notcontains $false) {
if ($allMatch) {
Write-Output $Config
}
}
Expand Down
57 changes: 26 additions & 31 deletions functions/private/Invoke-WinUtilISO.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,15 @@ function Invoke-WinUtilISOMountAndVerify {
try {
Mount-DiskImage -ImagePath $isoPath

# Add 30s timeout to prevent infinite hang on mount failure
$mountTimeout = 30; $mountElapsed = 0
do {
Start-Sleep -Milliseconds 500
$mountElapsed += 0.5
if ($mountElapsed -ge $mountTimeout) {
Dismount-DiskImage -ImagePath $isoPath -ErrorAction SilentlyContinue
throw "ISO mount timed out after $($mountTimeout)s - drive letter never appeared."
}
} until ((Get-DiskImage -ImagePath $isoPath | Get-Volume).DriveLetter)

$driveLetter = (Get-DiskImage -ImagePath $isoPath | Get-Volume).DriveLetter + ":"
Expand Down Expand Up @@ -216,7 +223,10 @@ function Invoke-WinUtilISOModify {
$sync["WPFWin11ISOStatusLog"].CaretIndex = $sync["WPFWin11ISOStatusLog"].Text.Length
$sync["WPFWin11ISOStatusLog"].ScrollToEnd()
})
Add-Content -Path (Join-Path $workDir "WinUtil_Win11ISO.log") -Value "[$ts] $msg"
# Write to host only; transcript captures it without file-locking conflicts
Write-Host "[$ts] $msg"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restore the ISO workdir log

During Win11 ISO modification and cleanup, this replaces the per-workdir WinUtil_Win11ISO.log write with Write-Host, so completed or failed ISO runs no longer leave a persistent log beside the copied media. If the UI closes or a user resumes/export-diagnoses a prior work directory, the detailed DISM/robocopy/customization history is gone; keep writing the workdir log while avoiding only the transcript-owned file.

Useful? React with 👍 / 👎.

# Log beside the working directory so it exists from the first line and survives cleanup
Add-Content -Path "$workDir.log" -Value "[$ts] $msg" -ErrorAction SilentlyContinue
}

function SetProgress($label, $pct) {
Expand Down Expand Up @@ -409,7 +419,8 @@ function Invoke-WinUtilISOCleanAndReset {
$sync["WPFWin11ISOStatusLog"].CaretIndex = $sync["WPFWin11ISOStatusLog"].Text.Length
$sync["WPFWin11ISOStatusLog"].ScrollToEnd()
})
Add-Content -Path (Join-Path $workDir "WinUtil_Win11ISO.log") -Value "[$ts] $msg"
# Write to host; transcript captures it without file-locking conflicts
Write-Host "[$ts] $msg"
}

function SetProgress($label, $pct) {
Expand Down Expand Up @@ -446,37 +457,21 @@ function Invoke-WinUtilISOCleanAndReset {
}
}

# Batch delete instead of file-by-file with per-100 progress
if ($workDir -and (Test-Path $workDir)) {
Log "Scanning files to delete in: $workDir"
SetProgress "Scanning files..." 5

$allFiles = @(Get-ChildItem -Path $workDir -File -Recurse -Force)
$allDirs = @(Get-ChildItem -Path $workDir -Directory -Recurse -Force |
Sort-Object { $_.FullName.Length } -Descending)
$total = $allFiles.Count
$deleted = 0

Log "Found $total files to delete."

foreach ($f in $allFiles) {
try { Remove-Item -Path $f.FullName -Force } catch { Log "WARNING: could not delete $($f.FullName): $_" }
$deleted++
if ($deleted % 100 -eq 0 -or $deleted -eq $total) {
$pct = [math]::Round(($deleted / [Math]::Max($total, 1)) * 85) + 5
SetProgress "Deleting files in $($f.Directory.Name)... ($deleted / $total)" $pct
}
}

foreach ($d in $allDirs) {
try { Remove-Item -Path $d.FullName -Force } catch { Log "WARNING: could not delete $($d.FullName): $_" }
}

try { Remove-Item -Path $workDir -Recurse -Force } catch { Log "WARNING: could not delete temp directory ${workDir}: $_" }

if (Test-Path $workDir) {
Log "WARNING: some items could not be deleted in $workDir"
} else {
Log "Deleting working directory: $workDir"
SetProgress "Cleaning up..." 10
try {
Remove-Item -Path $workDir -Recurse -Force -ErrorAction Stop
Log "Temp directory deleted successfully."
} catch {
Log "WARNING: batch delete failed, retrying file-by-file: $_"
Get-ChildItem -Path $workDir -File -Recurse -Force | ForEach-Object {
Remove-Item -Path $_.FullName -Force -ErrorAction SilentlyContinue
}
Remove-Item -Path $workDir -Recurse -Force -ErrorAction SilentlyContinue
if (Test-Path $workDir) { Log "WARNING: some items could not be deleted in $workDir" }
else { Log "Temp directory deleted on retry." }
}
} else {
Log "No temp directory found - resetting UI."
Expand Down
29 changes: 14 additions & 15 deletions functions/private/Invoke-WinUtilTweaks.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -40,46 +40,45 @@ function Invoke-WinUtilTweaks {
}
}
if ($sync.configs.tweaks.$CheckBox.service) {
$sync.configs.tweaks.$CheckBox.service | ForEach-Object {
foreach ($item in $sync.configs.tweaks.$CheckBox.service) {
$changeservice = $true

# The check for !($undo) is required, without it the script will throw an error for accessing unavailable member, which's the 'OriginalService' Property
if ($KeepServiceStartup -AND !($undo)) {
try {
# Check if the service exists
$service = Get-Service -Name $psitem.Name -ErrorAction Stop
if(!($service.StartType.ToString() -eq $psitem.$($values.OriginalService))) {
$service = Get-Service -Name $item.Name -ErrorAction Stop
if(!($service.StartType.ToString() -eq $item.$($values.OriginalService))) {
$changeservice = $false
}
} catch [System.ServiceProcess.ServiceNotFoundException] {
Write-Warning "Service $($psitem.Name) was not found."
Write-Warning "Service $($item.Name) was not found."
}
}

if ($changeservice) {
Set-WinUtilService -Name $psitem.Name -StartupType $psitem.$($values.Service)
Set-WinUtilService -Name $item.Name -StartupType $item.$($values.Service)
}
}
}
if ($sync.configs.tweaks.$CheckBox.registry) {
$sync.configs.tweaks.$CheckBox.registry | Where-Object { -not $psitem.Values } | ForEach-Object {
Set-WinUtilRegistry -Name $psitem.Name -Path $psitem.Path -Type $psitem.Type -Value $psitem.$($values.registry)
foreach ($reg in $sync.configs.tweaks.$CheckBox.registry) {
if ($reg.Values) { continue }
Set-WinUtilRegistry -Name $reg.Name -Path $reg.Path -Type $reg.Type -Value $reg.$($values.registry)
}
}
if ($sync.configs.tweaks.$CheckBox.$($values.ScriptType)) {
$sync.configs.tweaks.$CheckBox.$($values.ScriptType) | ForEach-Object {
$Scriptblock = [scriptblock]::Create($psitem)
foreach ($scr in $sync.configs.tweaks.$CheckBox.$($values.ScriptType)) {
$Scriptblock = [scriptblock]::Create($scr)
Invoke-WinUtilScript -ScriptBlock $scriptblock -Name $CheckBox
}
}

if (!$undo) {
if($sync.configs.tweaks.$CheckBox.appx) {
$sync.configs.tweaks.$CheckBox.appx | ForEach-Object {
Remove-WinUtilAPPX -Name $psitem
}
Remove-WinUtilProvisionedAPPX -PackageList $sync.configs.tweaks.$CheckBox.appx
if (!$undo -and $sync.configs.tweaks.$CheckBox.appx) {
foreach ($pkg in $sync.configs.tweaks.$CheckBox.appx) {
Remove-WinUtilAPPX -Name $pkg
}
Remove-WinUtilProvisionedAPPX -PackageList $sync.configs.tweaks.$CheckBox.appx
}
Write-WinUtilLog -Component "Tweaks" -Message "$action tweak completed: $CheckBox"
}
37 changes: 8 additions & 29 deletions functions/private/Show-CustomDialog.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -130,57 +130,36 @@ function Show-CustomDialog {

$dialog.Content = $border

# Create a grid for layout inside the Border
$grid = New-Object Windows.Controls.Grid
$border.Child = $grid

# Uncomment the following line to show gridlines
#$grid.ShowGridLines = $true

# Add the following line to set the background color of the grid
$grid.Background = [Windows.Media.Brushes]::Transparent
# Add the following line to make the Grid stretch
$grid.HorizontalAlignment = [Windows.HorizontalAlignment]::Stretch
$grid.VerticalAlignment = [Windows.VerticalAlignment]::Stretch

# Add the following line to make the Border stretch
$border.HorizontalAlignment = [Windows.HorizontalAlignment]::Stretch
$border.VerticalAlignment = [Windows.VerticalAlignment]::Stretch

# Set up Row Definitions
$row0 = New-Object Windows.Controls.RowDefinition
$row0.Height = [Windows.GridLength]::Auto

$row1 = New-Object Windows.Controls.RowDefinition
$row1.Height = [Windows.GridLength]::new(1, [Windows.GridUnitType]::Star)

$row2 = New-Object Windows.Controls.RowDefinition
$row2.Height = [Windows.GridLength]::Auto

# Add Row Definitions to Grid
$row0 = New-Object Windows.Controls.RowDefinition; $row0.Height = [Windows.GridLength]::Auto
$row1 = New-Object Windows.Controls.RowDefinition; $row1.Height = [Windows.GridLength]::new(1, [Windows.GridUnitType]::Star)
$row2 = New-Object Windows.Controls.RowDefinition; $row2.Height = [Windows.GridLength]::Auto
$grid.RowDefinitions.Add($row0)
$grid.RowDefinitions.Add($row1)
$grid.RowDefinitions.Add($row2)

# Add StackPanel for horizontal layout with margins
$stackPanel = New-Object Windows.Controls.StackPanel
$stackPanel.Margin = New-Object Windows.Thickness(10) # Add margins around the stack panel
$stackPanel.Margin = New-Object Windows.Thickness(10)
$stackPanel.Orientation = [Windows.Controls.Orientation]::Horizontal
$stackPanel.HorizontalAlignment = [Windows.HorizontalAlignment]::Left # Align to the left
$stackPanel.VerticalAlignment = [Windows.VerticalAlignment]::Top # Align to the top

$stackPanel.HorizontalAlignment = [Windows.HorizontalAlignment]::Left
$stackPanel.VerticalAlignment = [Windows.VerticalAlignment]::Top
$grid.Children.Add($stackPanel)
[Windows.Controls.Grid]::SetRow($stackPanel, 0) # Set the row to the second row (0-based index)
[Windows.Controls.Grid]::SetRow($stackPanel, 0)

# Add SVG path to the stack panel
$stackPanel.Children.Add((Invoke-WinUtilAssets -Type "logo" -Size $LogoSize))

# Add "Winutil" text
$winutilTextBlock = New-Object Windows.Controls.TextBlock
$winutilTextBlock.Text = "WinUtil"
$winutilTextBlock.FontSize = $HeaderFontSize
$winutilTextBlock.Foreground = $LogoColor
$winutilTextBlock.Margin = New-Object Windows.Thickness(10, 10, 10, 5) # Add margins around the text block
$winutilTextBlock.Margin = New-Object Windows.Thickness(10, 10, 10, 5)
$stackPanel.Children.Add($winutilTextBlock)
# Add TextBlock for information with text wrapping and margins
$messageTextBlock = New-Object Windows.Controls.TextBlock
Expand Down
Loading