diff --git a/docs/src/content/docs/code-reference/architecture.mdx b/docs/src/content/docs/code-reference/architecture.mdx index 000e621111..7a29f82fb4 100644 --- a/docs/src/content/docs/code-reference/architecture.mdx +++ b/docs/src/content/docs/code-reference/architecture.mdx @@ -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 diff --git a/functions/private/Find-AppsByNameOrDescription.ps1 b/functions/private/Find-AppsByNameOrDescription.ps1 index 44f4003cb5..646a81a5ee 100644 --- a/functions/private/Find-AppsByNameOrDescription.ps1 +++ b/functions/private/Find-AppsByNameOrDescription.ps1 @@ -91,22 +91,17 @@ 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] } @@ -114,19 +109,16 @@ function Find-AppsByNameOrDescription { 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 } } diff --git a/functions/private/Find-TweaksByNameOrDescription.ps1 b/functions/private/Find-TweaksByNameOrDescription.ps1 index a8dd52efe6..62fc543c5b 100644 --- a/functions/private/Find-TweaksByNameOrDescription.ps1 +++ b/functions/private/Find-TweaksByNameOrDescription.ps1 @@ -1,321 +1,128 @@ function Find-TweaksByNameOrDescription { <# - .SYNOPSIS - Searches through the Tweaks on the Tweaks Tab and hides all entries that do not match the search string - - .DESCRIPTION - Filters tweak entries by name or description using literal string matching (no wildcard expansion). - Respects collapsed category state and handles null $sync gracefully. - Safe for rapid keystroke events; no terminal spam on error conditions. - - .PARAMETER SearchString - The string to be searched for. Wildcards are treated as literal characters. - - .NOTES - - Uses module-scope $sync (resolved via global/script fallback if needed) - - Performs literal matching (no wildcard expansion) - - Safely handles missing UI elements and null properties - - Protected by try/catch to prevent UI thread crashes - - PowerShell 5.1 compatible (no ternary operators, no advanced language features) + .SYNOPSIS + Filters tweak/appx entries by name or description using literal string matching. + .PARAMETER SearchString + The string to search for. Treated as a literal (no wildcard expansion). #> param( - [Parameter(Mandatory = $false)] [string]$SearchString = "" ) - # ------------------------------------------------------------------------------ - # 1. RESOLVE $SYNC WITH MULTI-LEVEL FALLBACK - # ------------------------------------------------------------------------------ - - if ($null -eq $Sync) { - $Sync = $global:sync - if ($null -eq $Sync) { - $Sync = $script:sync - } - } - - # Validate that $Sync exists and has required structure - if ($null -eq $Sync) { - # Silent return - function called on every keystroke; no warning spam - return - } + # $sync is always in scope in the compiled script - no multi-level fallback needed + if ($null -eq $sync -or $null -eq $sync.Form) { return } - if ($null -eq $Sync.Form) { - # Silent return - form not yet initialized - return + if ($null -eq $sync.TweakCategoryAutoExpanded) { + $sync.TweakCategoryAutoExpanded = @{} } - # ------------------------------------------------------------------------------ - # 2. GET REFERENCE TO TWEAKS OR APPX PANEL - # ------------------------------------------------------------------------------ - - $panelName = "tweakspanel" - if ($null -ne $Sync.currentTab -and $Sync.currentTab -eq "AppX") { - $panelName = "appxpanel" + $panelName = if ($sync.currentTab -eq "AppX") { "appxpanel" } else { "tweakspanel" } + try { $tweaksPanel = $sync.Form.FindName($panelName) } catch { return } + if ($null -eq $tweaksPanel) { return } + + # --- Helper: extract searchable text from a DockPanel or StackPanel item --- + function Get-ItemSearchText($item) { + $text = ""; $tip = "" + if ($item -is [Windows.Controls.DockPanel]) { + $label = $item.Children | Where-Object { $_ -is [Windows.Controls.Label] } | Select-Object -First 1 + if ($label) { $text = [string]$label.Content; $tip = [string]$label.ToolTip } + } elseif ($item -is [Windows.Controls.StackPanel]) { + $cb = $item.Children | Where-Object { $_ -is [Windows.Controls.CheckBox] } | Select-Object -First 1 + if ($cb) { $text = [string]$cb.Content; $tip = [string]$cb.ToolTip } + } + return @{ Text = $text; Tip = $tip } } - $tweaksPanel = $null try { - $tweaksPanel = $Sync.Form.FindName($panelName) - } - catch { - # Silent return - panel not found or disposed - return - } - - if ($null -eq $tweaksPanel) { - # Silent return - panel doesn't exist - return - } - - # ------------------------------------------------------------------------------ - # 3. HANDLE EMPTY/WHITESPACE SEARCH STRING - RESET TO DEFAULT STATE - # ------------------------------------------------------------------------------ - - if ([string]::IsNullOrWhiteSpace($SearchString)) { - try { - $tweaksPanel.Children | ForEach-Object { - $categoryBorder = $_ - - # Safely set visibility - if ($null -ne $categoryBorder) { - $categoryBorder.Visibility = [Windows.Visibility]::Visible - } - - # Process each category - if ($categoryBorder -is [Windows.Controls.Border]) { - $dockPanel = $null - if ($null -ne $categoryBorder.Child) { - $dockPanel = $categoryBorder.Child - } - - if ($dockPanel -is [Windows.Controls.DockPanel]) { - $container = $dockPanel.Children | Where-Object { $_ -is [Windows.Controls.ItemsControl] -or $_ -is [Windows.Controls.StackPanel] -or $_ -is [Windows.Controls.ScrollViewer] -or $_.GetType().Name -eq "ItemsControl" } | Select-Object -First 1 - - if ($null -ne $container) { - $targetPanel = if ($container.PSObject.Properties['Content'] -and $null -ne $container.Content) { $container.Content } else { $container } - $items = $null - if ($targetPanel -is [Windows.Controls.ItemsControl] -or $targetPanel.GetType().Name -eq "ItemsControl") { - $items = $targetPanel.Items - } - else { - $items = $targetPanel.Children - } - # Show all items in the category - foreach ($item in $items) { - if ($null -ne $item) { - # Check if it's a category label (first Label in the container) - if ($item -is [Windows.Controls.Label] -or $item.GetType().Name -eq "Label") { - $item.Visibility = [Windows.Visibility]::Visible - } - elseif ($item -is [Windows.Controls.DockPanel] -or $item -is [Windows.Controls.StackPanel] -or $item.GetType().Name -eq "DockPanel" -or $item.GetType().Name -eq "StackPanel") { - # Show all checkbox containers - $item.Visibility = [Windows.Visibility]::Visible - } + # Reset visibility when search is cleared + if ([string]::IsNullOrWhiteSpace($SearchString)) { + foreach ($categoryBorder in $tweaksPanel.Children) { + if ($null -eq $categoryBorder) { continue } + $categoryBorder.Visibility = [Windows.Visibility]::Visible + if ($categoryBorder -is [Windows.Controls.Border] -and $categoryBorder.Child -is [Windows.Controls.DockPanel]) { + $container = $categoryBorder.Child.Children | Where-Object { $_ -is [Windows.Controls.ItemsControl] -or $_ -is [Windows.Controls.StackPanel] -or $_ -is [Windows.Controls.ScrollViewer] } | Select-Object -First 1 + if ($container) { + $targetPanel = if ($container -is [Windows.Controls.ScrollViewer]) { $container.Content } else { $container } + $items = if ($targetPanel -is [Windows.Controls.ItemsControl]) { $targetPanel.Items } else { $targetPanel.Children } + foreach ($item in $items) { + if ($null -eq $item) { continue } + if ($item -is [Windows.Controls.Label]) { + $item.Visibility = [Windows.Visibility]::Visible + # A category that filtering expanded goes back to how the user left it + $labelStr = [string]$item.Content + $categoryName = $labelStr -replace '^[+-] ', '' + if ($sync.TweakCategoryAutoExpanded.ContainsKey($categoryName)) { + $item.Content = $labelStr -replace "^- ", "+ " + $labelStr = [string]$item.Content + $sync.TweakCategoryAutoExpanded.Remove($categoryName) } + # Respect collapsed state: labels starting with "+" keep items collapsed + if ($labelStr.StartsWith("+ ")) { + $collapsed = $true + } else { + $collapsed = $false + } + } else { + $item.Visibility = if ($collapsed) { [Windows.Visibility]::Collapsed } else { [Windows.Visibility]::Visible } } } } } } - } - catch { - # Silent catch - UI element may be disposed - $null = $_ - } - - return - } - - # ------------------------------------------------------------------------------ - # 4. PERFORM LITERAL SEARCH (NO WILDCARD EXPANSION) - # ------------------------------------------------------------------------------ - - try { - # Normalize search term once for the entire operation - $searchTerm = $SearchString - if ($null -eq $searchTerm) { - $searchTerm = "" + return } - # Iterate through all categories - $tweaksPanel.Children | ForEach-Object { - $categoryBorder = $_ + # Perform literal search + foreach ($categoryBorder in $tweaksPanel.Children) { $categoryHasMatch = $false + if (-not ($categoryBorder -is [Windows.Controls.Border])) { continue } + if (-not ($categoryBorder.Child -is [Windows.Controls.DockPanel])) { continue } - if ($categoryBorder -is [Windows.Controls.Border]) { - $dockPanel = $null - if ($null -ne $categoryBorder.Child) { - $dockPanel = $categoryBorder.Child - } - - if ($dockPanel -is [Windows.Controls.DockPanel]) { - $container = $dockPanel.Children | Where-Object { $_ -is [Windows.Controls.ItemsControl] -or $_ -is [Windows.Controls.StackPanel] -or $_ -is [Windows.Controls.ScrollViewer] -or $_.GetType().Name -eq "ItemsControl" } | Select-Object -First 1 - - if ($null -ne $container) { - $categoryLabel = $null - - $targetPanel = if ($container.PSObject.Properties['Content'] -and $null -ne $container.Content) { $container.Content } else { $container } - $items = $null - if ($targetPanel -is [Windows.Controls.ItemsControl] -or $targetPanel.GetType().Name -eq "ItemsControl") { - $items = $targetPanel.Items - } - else { - $items = $targetPanel.Children - } - # Process all items (checkboxes, labels, panels) in the container - foreach ($item in $items) { - if ($null -eq $item) { - continue - } - - # ------------------------------------------------------------ - # Check if this is a category label (usually first Label) - # ------------------------------------------------------------ - - if ($item -is [Windows.Controls.Label] -or $item.GetType().Name -eq "Label") { - $categoryLabel = $item - # Initially hide category label; show it only if matches found - $item.Visibility = [Windows.Visibility]::Collapsed - } - - # ------------------------------------------------------------ - # Check if this is a DockPanel containing a tweak checkbox - # ------------------------------------------------------------ - - elseif ($item -is [Windows.Controls.DockPanel] -or $item.GetType().Name -eq "DockPanel") { - $checkbox = $null - $label = $null - - # Safely extract checkbox and label - $checkbox = $item.Children | Where-Object { $_ -is [Windows.Controls.CheckBox] -or $_.GetType().Name -eq "CheckBox" } | Select-Object -First 1 - $label = $item.Children | Where-Object { $_ -is [Windows.Controls.Label] -or $_.GetType().Name -eq "Label" } | Select-Object -First 1 - - # Check if tweak matches search criteria - $itemMatches = $false - - if ($null -ne $label) { - $labelContent = $label.Content - $labelToolTip = $label.ToolTip - - # Safely null-check properties - if ($null -eq $labelContent) { - $labelContent = "" - } - if ($null -eq $labelToolTip) { - $labelToolTip = "" - } - - # Convert to string and perform LITERAL matching - $labelContentStr = [string]$labelContent - $labelToolTipStr = [string]$labelToolTip - - # Use IndexOf for literal matching (no wildcard interpretation) - $contentMatch = $labelContentStr.IndexOf($searchTerm, [System.StringComparison]::OrdinalIgnoreCase) -ge 0 - $toolTipMatch = $labelToolTipStr.IndexOf($searchTerm, [System.StringComparison]::OrdinalIgnoreCase) -ge 0 - - if ($contentMatch -or $toolTipMatch) { - $itemMatches = $true - } - } - - # Set visibility based on match result - if ($itemMatches) { - $item.Visibility = [Windows.Visibility]::Visible - $categoryHasMatch = $true - } - else { - $item.Visibility = [Windows.Visibility]::Collapsed - } - } - - # ------------------------------------------------------------ - # Check if this is a StackPanel containing a tweak checkbox - # ------------------------------------------------------------ - - elseif ($item -is [Windows.Controls.StackPanel] -or $item.GetType().Name -eq "StackPanel") { - $checkbox = $null - $checkbox = $item.Children | Where-Object { $_ -is [Windows.Controls.CheckBox] -or $_.GetType().Name -eq "CheckBox" } | Select-Object -First 1 + $container = $categoryBorder.Child.Children | Where-Object { $_ -is [Windows.Controls.ItemsControl] -or $_ -is [Windows.Controls.StackPanel] -or $_ -is [Windows.Controls.ScrollViewer] } | Select-Object -First 1 + if ($null -eq $container) { continue } - $itemMatches = $false + $targetPanel = if ($container -is [Windows.Controls.ScrollViewer]) { $container.Content } else { $container } + $items = if ($targetPanel -is [Windows.Controls.ItemsControl]) { $targetPanel.Items } else { $targetPanel.Children } - if ($null -ne $checkbox) { - $checkboxContent = $checkbox.Content - $checkboxToolTip = $checkbox.ToolTip + $categoryLabel = $null + for ($i = 0; $i -lt $items.Count; $i++) { + $item = $items[$i] + if ($null -eq $item) { continue } - # Safely null-check properties - if ($null -eq $checkboxContent) { - $checkboxContent = "" - } - if ($null -eq $checkboxToolTip) { - $checkboxToolTip = "" - } - - # Convert to string and perform LITERAL matching - $checkboxContentStr = [string]$checkboxContent - $checkboxToolTipStr = [string]$checkboxToolTip - - # Use IndexOf for literal matching (no wildcard interpretation) - $contentMatch = $checkboxContentStr.IndexOf($searchTerm, [System.StringComparison]::OrdinalIgnoreCase) -ge 0 - $toolTipMatch = $checkboxToolTipStr.IndexOf($searchTerm, [System.StringComparison]::OrdinalIgnoreCase) -ge 0 - - if ($contentMatch -or $toolTipMatch) { - $itemMatches = $true - } - } - - # Set visibility based on match result - if ($itemMatches) { - $item.Visibility = [Windows.Visibility]::Visible - $categoryHasMatch = $true - } - else { - $item.Visibility = [Windows.Visibility]::Collapsed - } - } - } - - # ------------------------------------------------------------ - # Update category label visibility and expanded/collapsed state - # ------------------------------------------------------------ - - if ($categoryHasMatch) { - # Show category label - if ($null -ne $categoryLabel) { - $categoryLabel.Visibility = [Windows.Visibility]::Visible - - # Update category label to expanded state (change "+" to "-") - $labelContent = $categoryLabel.Content - if ($null -ne $labelContent) { - $labelStr = [string]$labelContent + if ($item -is [Windows.Controls.Label]) { + $categoryLabel = $item + $item.Visibility = [Windows.Visibility]::Collapsed + continue + } - # Safe string replacement without -replace regex - if ($labelStr.StartsWith("+ ")) { - $expandedLabel = "- " + $labelStr.Substring(2) - $categoryLabel.Content = $expandedLabel - } - } - } - } + # Unified search for both DockPanel and StackPanel items + if ($item -is [Windows.Controls.DockPanel] -or $item -is [Windows.Controls.StackPanel]) { + $search = Get-ItemSearchText $item + $isMatch = ($search.Text -and $search.Text.IndexOf($SearchString, [System.StringComparison]::OrdinalIgnoreCase) -ge 0) -or + ($search.Tip -and $search.Tip.IndexOf($SearchString, [System.StringComparison]::OrdinalIgnoreCase) -ge 0) + + if ($isMatch) { + $item.Visibility = [Windows.Visibility]::Visible + $categoryHasMatch = $true + } else { + $item.Visibility = [Windows.Visibility]::Collapsed } } + } - # ---------------------------------------------------------------- - # Set category border visibility based on whether it has matches - # ---------------------------------------------------------------- - - if ($categoryHasMatch) { - $categoryBorder.Visibility = [Windows.Visibility]::Visible - } - else { - $categoryBorder.Visibility = [Windows.Visibility]::Collapsed + # Update category label and border visibility + if ($categoryHasMatch -and $null -ne $categoryLabel) { + $categoryLabel.Visibility = [Windows.Visibility]::Visible + $labelStr = [string]$categoryLabel.Content + if ($labelStr.StartsWith("+ ")) { + $categoryLabel.Content = "- " + $labelStr.Substring(2) + $sync.TweakCategoryAutoExpanded[($labelStr.Substring(2))] = $true } } + $categoryBorder.Visibility = if ($categoryHasMatch) { [Windows.Visibility]::Visible } else { [Windows.Visibility]::Collapsed } } - } - catch { - # Silent catch - UI elements may be disposed or in unexpected state - # Do not log to terminal as this function is called on every keystroke - $null = $_ + } catch { + # Log instead of silently swallowing - but only at DEBUG to avoid keystroke spam + Write-WinUtilLog -Level "DEBUG" -Component "Search" -Message "Tweaks search error: $($_.Exception.Message)" } } diff --git a/functions/private/Invoke-WinUtilCurrentSystem.ps1 b/functions/private/Invoke-WinUtilCurrentSystem.ps1 index 68594ab6f3..b5f1e6324f 100644 --- a/functions/private/Invoke-WinUtilCurrentSystem.ps1 +++ b/functions/private/Invoke-WinUtilCurrentSystem.ps1 @@ -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 } } } @@ -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 } } } @@ -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 @@ -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 } } } @@ -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) { + $allMatch = $false + break } } + if (-not $allMatch) { break } } - if ($values -notcontains $false) { + if ($allMatch) { Write-Output $Config } } diff --git a/functions/private/Invoke-WinUtilISO.ps1 b/functions/private/Invoke-WinUtilISO.ps1 index af9f6f3d9d..71678026ed 100644 --- a/functions/private/Invoke-WinUtilISO.ps1 +++ b/functions/private/Invoke-WinUtilISO.ps1 @@ -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 + ":" @@ -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" + # 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) { @@ -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) { @@ -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." diff --git a/functions/private/Invoke-WinUtilTweaks.ps1 b/functions/private/Invoke-WinUtilTweaks.ps1 index 87673ea8fa..0d35fdf24d 100644 --- a/functions/private/Invoke-WinUtilTweaks.ps1 +++ b/functions/private/Invoke-WinUtilTweaks.ps1 @@ -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" } diff --git a/functions/private/Show-CustomDialog.ps1 b/functions/private/Show-CustomDialog.ps1 index 9ae289e6e0..1abbb0c301 100644 --- a/functions/private/Show-CustomDialog.ps1 +++ b/functions/private/Show-CustomDialog.ps1 @@ -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 diff --git a/functions/private/Test-WinUtilPackageManager.ps1 b/functions/private/Test-WinUtilPackageManager.ps1 index ed7546d85b..2bc6fca80e 100644 --- a/functions/private/Test-WinUtilPackageManager.ps1 +++ b/functions/private/Test-WinUtilPackageManager.ps1 @@ -1,49 +1,30 @@ function Test-WinUtilPackageManager { <# - .SYNOPSIS - Checks if WinGet and/or Choco are installed - + Checks if WinGet and/or Choco are installed. .PARAMETER winget - Check if WinGet is installed - + Check if WinGet is installed. .PARAMETER choco - Check if Chocolatey is installed - + Check if Chocolatey is installed. #> - Param( [System.Management.Automation.SwitchParameter]$winget, [System.Management.Automation.SwitchParameter]$choco ) - if ($winget) { - if (Get-Command winget -ErrorAction SilentlyContinue) { - Write-Host "===========================================" -ForegroundColor Green - Write-Host "--- WinGet is installed ---" -ForegroundColor Green - Write-Host "===========================================" -ForegroundColor Green - $status = "installed" - } else { - Write-Host "===========================================" -ForegroundColor Red - Write-Host "--- WinGet is not installed ---" -ForegroundColor Red - Write-Host "===========================================" -ForegroundColor Red - $status = "not-installed" - } - } + # Handle missing switch - callers rely on the return value + if (-not $winget -and -not $choco) { return "not-installed" } - if ($choco) { - if (Get-Command choco -ErrorAction SilentlyContinue) { - Write-Host "===========================================" -ForegroundColor Green - Write-Host "--- Chocolatey is installed ---" -ForegroundColor Green - Write-Host "===========================================" -ForegroundColor Green - $status = "installed" - } else { - Write-Host "===========================================" -ForegroundColor Red - Write-Host "--- Chocolatey is not installed ---" -ForegroundColor Red - Write-Host "===========================================" -ForegroundColor Red - $status = "not-installed" + $cmds = @() + if ($winget) { $cmds += "winget" } + if ($choco) { $cmds += "choco" } + + foreach ($cmd in $cmds) { + if (-not (Get-Command $cmd -ErrorAction SilentlyContinue)) { + Write-Host "$cmd is not installed" -ForegroundColor Red + return "not-installed" } + Write-Host "$cmd is installed" -ForegroundColor Green } - - return $status + return "installed" } diff --git a/functions/private/Write-WinUtilLog.ps1 b/functions/private/Write-WinUtilLog.ps1 index da9e13ab7a..4ff6f4bc91 100644 --- a/functions/private/Write-WinUtilLog.ps1 +++ b/functions/private/Write-WinUtilLog.ps1 @@ -1,18 +1,13 @@ function Write-WinUtilLog { <# - .SYNOPSIS Writes a timestamped WinUtil log entry to the active session log. - .PARAMETER Message The message to write. - .PARAMETER Level The severity level for the log entry. - .PARAMETER Component The WinUtil component producing the log entry. - #> param ( [Parameter(Mandatory = $true)] @@ -25,56 +20,39 @@ function Write-WinUtilLog { ) try { + # Single resolution chain instead of 4 separate if-blocks $logPath = $null - $transcriptPath = $null - if ($null -ne $sync -and $sync.ContainsKey("logPath")) { - $logPath = $sync.logPath - } - - if ($null -ne $sync -and $sync.ContainsKey("transcriptPath")) { - $transcriptPath = $sync.transcriptPath - } - - if ([string]::IsNullOrWhiteSpace($logPath) -and -not [string]::IsNullOrWhiteSpace($transcriptPath)) { - $logPath = $transcriptPath - } - - if ([string]::IsNullOrWhiteSpace($logPath) -and $null -ne $sync -and $sync.ContainsKey("winutildir")) { - $logDirectory = Join-Path $sync.winutildir "logs" - $logPath = Join-Path $logDirectory "winutil_$(Get-Date -Format "yyyy-MM-dd_HH-mm-ss").log" - $sync.logPath = $logPath + $isTranscript = $false + if ($null -ne $sync) { + if ($sync.ContainsKey("logPath") -and -not [string]::IsNullOrWhiteSpace($sync.logPath)) { + $logPath = $sync.logPath + } elseif ($sync.ContainsKey("transcriptPath") -and -not [string]::IsNullOrWhiteSpace($sync.transcriptPath)) { + $logPath = $sync.transcriptPath + } elseif ($sync.ContainsKey("winutildir")) { + $logPath = Join-Path (Join-Path $sync.winutildir "logs") "winutil_$(Get-Date -Format 'yyyy-MM-dd_HH-mm-ss').log" + $sync.logPath = $logPath + } + if ($sync.ContainsKey("transcriptPath") -and -not [string]::IsNullOrWhiteSpace($sync.transcriptPath) -and $logPath -eq $sync.transcriptPath) { + $isTranscript = $true + } } - if ([string]::IsNullOrWhiteSpace($logPath) -and -not [string]::IsNullOrWhiteSpace($env:LocalAppData)) { if ([string]::IsNullOrWhiteSpace($script:WinUtilLogPath)) { - $logDirectory = Join-Path (Join-Path $env:LocalAppData "winutil") "logs" - $script:WinUtilLogPath = Join-Path $logDirectory "winutil_$(Get-Date -Format "yyyy-MM-dd_HH-mm-ss").log" + $script:WinUtilLogPath = Join-Path (Join-Path (Join-Path $env:LocalAppData "winutil") "logs") "winutil_$(Get-Date -Format 'yyyy-MM-dd_HH-mm-ss').log" } $logPath = $script:WinUtilLogPath } + if ([string]::IsNullOrWhiteSpace($logPath)) { return } - if ([string]::IsNullOrWhiteSpace($logPath)) { - return - } + $logDir = Split-Path -Path $logPath -Parent + if (-not (Test-Path $logDir)) { New-Item -Path $logDir -ItemType Directory -Force | Out-Null } - $logDirectory = Split-Path -Path $logPath -Parent - if (-not (Test-Path $logDirectory)) { - New-Item -Path $logDirectory -ItemType Directory -Force | Out-Null - } - - $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss.fff" - $line = "[$timestamp] [$Level] [$Component] $Message" + $line = "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss.fff')] [$Level] [$Component] $Message" - if (-not [string]::IsNullOrWhiteSpace($transcriptPath) -and $logPath -eq $transcriptPath) { - Write-Host $line - return - } + if ($isTranscript) { Write-Host $line; return } - try { - Add-Content -Path $logPath -Value $line -Encoding UTF8 -ErrorAction Stop - } catch [System.IO.IOException] { - Write-Host $line - } + try { Add-Content -Path $logPath -Value $line -Encoding UTF8 -ErrorAction Stop } + catch [System.IO.IOException] { Write-Host $line } } catch { Write-Warning "Unable to write WinUtil log entry: $($_.Exception.Message)" } diff --git a/functions/public/Invoke-WPFFixesUpdate.ps1 b/functions/public/Invoke-WPFFixesUpdate.ps1 index 4d7cfcefa4..5c5f185ef8 100644 --- a/functions/public/Invoke-WPFFixesUpdate.ps1 +++ b/functions/public/Invoke-WPFFixesUpdate.ps1 @@ -42,14 +42,19 @@ function Invoke-WPFFixesUpdate { Write-Progress -Id 0 -Activity "Repairing Windows Update" -Status "Stopping Windows Update Services..." -PercentComplete 10 # Stop the Windows Update Services - Write-Progress -Id 2 -ParentId 0 -Activity "Stopping Services" -Status "Stopping BITS..." -PercentComplete 0 - Stop-Service -Name BITS -Force - Write-Progress -Id 2 -ParentId 0 -Activity "Stopping Services" -Status "Stopping wuauserv..." -PercentComplete 20 - Stop-Service -Name wuauserv -Force - Write-Progress -Id 2 -ParentId 0 -Activity "Stopping Services" -Status "Stopping appidsvc..." -PercentComplete 40 - Stop-Service -Name appidsvc -Force - Write-Progress -Id 2 -ParentId 0 -Activity "Stopping Services" -Status "Stopping cryptsvc..." -PercentComplete 60 - Stop-Service -Name cryptsvc -Force + $services = @("BITS", "wuauserv", "appidsvc", "cryptsvc") + for ($i = 0; $i -lt $services.Count; $i++) { + $svc = $services[$i] + $pct = [int](($i / $services.Count) * 100) + Write-Progress -Id 2 -ParentId 0 -Activity "Stopping Services" -Status "Stopping $svc..." -PercentComplete $pct + try { + Stop-Service -Name $svc -Force -ErrorAction Stop + } catch { + Write-Progress -Id 2 -ParentId 0 -Activity "Stopping Services" -Status "Failed to stop $svc" -PercentComplete $pct + Set-WinUtilTaskbaritem -state "Error" -overlay "warning" + throw "Failed to stop service $svc - cannot continue with Windows Update repair: $_" + } + } Write-Progress -Id 2 -ParentId 0 -Activity "Stopping Services" -Status "Completed" -PercentComplete 100 @@ -102,7 +107,9 @@ function Invoke-WPFFixesUpdate { "wuweb.dll", "qmgr.dll", "qmgrprxy.dll", "wucltux.dll", "muweb.dll", "wuwebv.dll" ) foreach ($dll in $DLLs) { - Write-Progress -Id 5 -ParentId 0 -Activity "Reregistering DLLs" -Status "Registering $dll..." -PercentComplete ($i / $DLLs.Count * 100) + if ($i % 5 -eq 0 -or $i -eq ($DLLs.Count - 1)) { + Write-Progress -Id 5 -ParentId 0 -Activity "Reregistering DLLs" -Status "Registering $dll..." -PercentComplete (($i / $DLLs.Count) * 100) + } $i++ Start-Process -NoNewWindow -FilePath "regsvr32.exe" -ArgumentList "/s", $dll } @@ -212,15 +219,7 @@ function Invoke-WPFFixesUpdate { Write-Host "===============================================" # Remove the progress bars - Write-Progress -Id 0 -Activity "Repairing Windows Update" -Completed - Write-Progress -Id 1 -Activity "Scanning for corruption" -Completed - Write-Progress -Id 2 -Activity "Stopping Services" -Completed - Write-Progress -Id 3 -Activity "Renaming/Removing Files" -Completed - Write-Progress -Id 4 -Activity "Resetting the WU Service Security Descriptors" -Completed - Write-Progress -Id 5 -Activity "Reregistering DLLs" -Completed - Write-Progress -Id 6 -Activity "Removing Group Policy Windows Update settings" -Completed - Write-Progress -Id 7 -Activity "Resetting WinSock" -Completed - Write-Progress -Id 8 -Activity "Deleting BITS jobs" -Completed - Write-Progress -Id 9 -Activity "Starting Windows Update Services" -Completed - Write-Progress -Id 10 -Activity "Forcing discovery" -Completed + foreach ($id in 0..10) { + Write-Progress -Id $id -Activity "Completed" -Completed -ErrorAction SilentlyContinue + } } diff --git a/functions/public/Invoke-WPFUIElements.ps1 b/functions/public/Invoke-WPFUIElements.ps1 index eb50fae0b5..f1ca5d9136 100644 --- a/functions/public/Invoke-WPFUIElements.ps1 +++ b/functions/public/Invoke-WPFUIElements.ps1 @@ -53,22 +53,15 @@ function Invoke-WPFUIElements { $targetGrid.ColumnDefinitions.Add($colDef) | Out-Null } - # Convert PSCustomObject to Hashtable - $configHashtable = @{} - $configVariable.PSObject.Properties.Name | ForEach-Object { - $configHashtable[$_] = $configVariable.$_ - } - $radioButtonGroups = @{} - $organizedData = @{} - # Iterate through JSON data and organize by panel and category - foreach ($entry in $configHashtable.Keys) { - $entryInfo = $configHashtable[$entry] - # Create an object for the application + # Iterate through JSON data and organize by panel and category using generic lists + foreach ($prop in $configVariable.PSObject.Properties) { + $entryInfo = $prop.Value + $entryObject = [PSCustomObject]@{ - Name = $entry + Name = $prop.Name Category = $entryInfo.Category Content = $entryInfo.Content Panel = if ($entryInfo.Panel) { $entryInfo.Panel } else { "0" } @@ -80,20 +73,20 @@ function Invoke-WPFUIElements { Registry = $entryInfo.registry Checked = $entryInfo.Checked ButtonWidth = $entryInfo.ButtonWidth - GroupName = $entryInfo.GroupName # Added for RadioButton groupings + GroupName = $entryInfo.GroupName } - if (-not $organizedData.ContainsKey($entryObject.Panel)) { - $organizedData[$entryObject.Panel] = @{} + $panel = $entryObject.Panel + if (-not $organizedData.ContainsKey($panel)) { + $organizedData[$panel] = @{} } - if (-not $organizedData[$entryObject.Panel].ContainsKey($entryObject.Category)) { - $organizedData[$entryObject.Panel][$entryObject.Category] = @() + $cat = $entryObject.Category + if (-not $organizedData[$panel].ContainsKey($cat)) { + $organizedData[$panel][$cat] = [System.Collections.Generic.List[object]]::new() } - # Store application data in an array under the category - $organizedData[$entryObject.Panel][$entryObject.Category] += $entryObject - + $organizedData[$panel][$cat].Add($entryObject) } # Initialize panel count diff --git a/pester/sanity.Tests.ps1 b/pester/sanity.Tests.ps1 index ba1d5d4b52..45fcc6b272 100644 --- a/pester/sanity.Tests.ps1 +++ b/pester/sanity.Tests.ps1 @@ -33,7 +33,7 @@ BeforeAll { $env:WINUTIL_TEST_PARSE_PATHS = @($Path) -join [Environment]::NewLine $parseScript = @' $ErrorActionPreference = 'Stop' -$paths = $env:WINUTIL_TEST_PARSE_PATHS -split "`r?`n" | Where-Object { $_ } +$paths = $env:WINUTIL_TEST_PARSE_PATHS -split '\r?\n' | Where-Object { $_ } $failed = @() foreach ($path in $paths) { @@ -43,7 +43,7 @@ foreach ($path in $paths) { if ($syntaxErrors.Count -ne 0) { $messages = $syntaxErrors | ForEach-Object { $_.Message } - $failed += "[$path] $($messages -join '; ')" + $failed += ('[' + $path + '] ' + ($messages -join '; ')) } } diff --git a/pester/search-filter.Tests.ps1 b/pester/search-filter.Tests.ps1 index 742114243e..9c32f35014 100644 --- a/pester/search-filter.Tests.ps1 +++ b/pester/search-filter.Tests.ps1 @@ -466,23 +466,28 @@ Describe "Find-TweaksByNameOrDescription" { Remove-WinUtilSearchGlobals } - It "restores category labels and tweak item visibility for empty search" { - $labelItem = New-WinUtilTweakLabelItem -Content "Disable Telemetry" -ToolTip "Stop tracking" - $stackItem = New-WinUtilTweakCheckboxItem -Content "Show Extensions" -ToolTip "File extension display" - $category = New-WinUtilTweakCategory -Label "+ Privacy" -Items @($labelItem, $stackItem) - $labelItem.Visibility = [Windows.Visibility]::Collapsed - $stackItem.Visibility = [Windows.Visibility]::Collapsed - $category.Label.Visibility = [Windows.Visibility]::Collapsed - $category.Border.Visibility = [Windows.Visibility]::Collapsed - $panel = New-WinUtilTweakPanel -Categories @($category) + It "restores category labels and respects collapsed category state for empty search" { + $collapsedItem = New-WinUtilTweakLabelItem -Content "Disable Telemetry" -ToolTip "Stop tracking" + $expandedItem = New-WinUtilTweakCheckboxItem -Content "Show Extensions" -ToolTip "File extension display" + $collapsedCategory = New-WinUtilTweakCategory -Label "+ Privacy" -Items @($collapsedItem) + $expandedCategory = New-WinUtilTweakCategory -Label "- Explorer" -Items @($expandedItem) + $expandedItem.Visibility = [Windows.Visibility]::Collapsed + $collapsedCategory.Label.Visibility = [Windows.Visibility]::Collapsed + $collapsedCategory.Border.Visibility = [Windows.Visibility]::Collapsed + $expandedCategory.Border.Visibility = [Windows.Visibility]::Collapsed + $panel = New-WinUtilTweakPanel -Categories @($collapsedCategory, $expandedCategory) New-WinUtilTweakSearchContext -TweaksPanel $panel Find-TweaksByNameOrDescription -SearchString "" - $category.Border.Visibility | Should -Be ([Windows.Visibility]::Visible) - $category.Label.Visibility | Should -Be ([Windows.Visibility]::Visible) - $labelItem.Visibility | Should -Be ([Windows.Visibility]::Visible) - $stackItem.Visibility | Should -Be ([Windows.Visibility]::Visible) + $collapsedCategory.Border.Visibility | Should -Be ([Windows.Visibility]::Visible) + $collapsedCategory.Label.Visibility | Should -Be ([Windows.Visibility]::Visible) + $collapsedCategory.Label.Content | Should -Be "+ Privacy" + $collapsedItem.Visibility | Should -Be ([Windows.Visibility]::Collapsed) + $expandedCategory.Border.Visibility | Should -Be ([Windows.Visibility]::Visible) + $expandedCategory.Label.Visibility | Should -Be ([Windows.Visibility]::Visible) + $expandedCategory.Label.Content | Should -Be "- Explorer" + $expandedItem.Visibility | Should -Be ([Windows.Visibility]::Visible) } It "shows tweak matches by label tooltip and checkbox content" { diff --git a/scripts/main.ps1 b/scripts/main.ps1 index a37700199a..f956fb6f0c 100644 --- a/scripts/main.ps1 +++ b/scripts/main.ps1 @@ -146,28 +146,16 @@ switch ($sync.preferences.packagemanager) { "Winget" {$sync.WingetRadioButton.IsChecked = $true; break} } +# Merged Button/ToggleButton registration, direct .GetType().Name (no pipeline overhead) $sync.keys | ForEach-Object { - if($sync.$psitem) { - if($($sync["$psitem"].GetType() | Select-Object -ExpandProperty Name) -eq "ToggleButton") { - if ($sync.Buttons -notcontains $psitem) { - $sync["$psitem"].Add_Click({ - [System.Object]$Sender = $args[0] - Invoke-WPFButton $Sender.name - }) - $sync.Buttons.Add($psitem) | Out-Null - } + if ($sync.$psitem -and $sync["$psitem"].GetType().Name -in "Button", "ToggleButton") { + if ($sync.Buttons -notcontains $psitem) { + $sync["$psitem"].Add_Click({ + [System.Object]$Sender = $args[0] + Invoke-WPFButton $Sender.name + }) + $sync.Buttons.Add($psitem) | Out-Null } - - if($($sync["$psitem"].GetType() | Select-Object -ExpandProperty Name) -eq "Button") { - if ($sync.Buttons -notcontains $psitem) { - $sync["$psitem"].Add_Click({ - [System.Object]$Sender = $args[0] - Invoke-WPFButton $Sender.name - }) - $sync.Buttons.Add($psitem) | Out-Null - } - } - } }