diff --git a/functions/private/Find-AppsByNameOrDescription.ps1 b/functions/private/Find-AppsByNameOrDescription.ps1 index 4bf55cb7..a4da2b56 100644 --- a/functions/private/Find-AppsByNameOrDescription.ps1 +++ b/functions/private/Find-AppsByNameOrDescription.ps1 @@ -3,78 +3,134 @@ function Find-AppsByNameOrDescription { .SYNOPSIS Searches through the Apps on the Install Tab and hides all entries that do not match the string + .DESCRIPTION + Filters application entries by name or description using literal string matching. + Respects collapsed category state and handles null $sync gracefully. + .PARAMETER SearchString - The string to be searched for + The string to be searched for. Wildcards are treated as literal characters. + + .NOTES + - Uses module-scope $sync (no parameter needed; inherits from caller's scope) + - Performs literal matching (no wildcard expansion) + - Safely handles missing hashtable keys and null UI elements + - Protected by try/catch to prevent UI thread crashes #> param( - [Parameter(Mandatory=$false)] + [Parameter(Mandatory = $false)] [string]$SearchString = "" ) - # Reset the visibility if the search string is empty or the search is cleared - if ([string]::IsNullOrWhiteSpace($SearchString)) { - $sync.ItemsControl.Items | ForEach-Object { - # Each item is a StackPanel container - $_.Visibility = [Windows.Visibility]::Visible + # Validate that $sync exists and has required structure + if ($null -eq $sync) { + Write-Warning "Find-AppsByNameOrDescription: Global `$sync not found. Aborting search." + return + } + + if ($null -eq $sync.ItemsControl) { + Write-Warning "Find-AppsByNameOrDescription: `$sync.ItemsControl not initialized. Aborting search." + return + } + + if ($null -eq $sync.configs -or $null -eq $sync.configs.applicationsHashtable) { + Write-Warning "Find-AppsByNameOrDescription: `$sync.configs.applicationsHashtable not initialized. Aborting search." + return + } + + try { + # Reset the visibility if the search string is empty or the search is cleared + if ([string]::IsNullOrWhiteSpace($SearchString)) { + $sync.ItemsControl.Items | ForEach-Object { + # Each item is a StackPanel container + $_.Visibility = [Windows.Visibility]::Visible + + if ($_.Children.Count -ge 2) { + $categoryLabel = $_.Children[0] + $wrapPanel = $_.Children[1] + + # Keep category label visible + $categoryLabel.Visibility = [Windows.Visibility]::Visible + + # Respect the collapsed state of categories (indicated by + prefix) + if ($categoryLabel.Content -like "+*") { + $wrapPanel.Visibility = [Windows.Visibility]::Collapsed + } + else { + $wrapPanel.Visibility = [Windows.Visibility]::Visible + } + + # Show all apps within the category + $wrapPanel.Children | ForEach-Object { + $_.Visibility = [Windows.Visibility]::Visible + } + } + } + return + } + + # Escape wildcard characters for literal matching + $escapedSearchString = [System.Management.Automation.WildcardPattern]::Escape($SearchString) + + # Perform search + $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 # Keep category label visible $categoryLabel.Visibility = [Windows.Visibility]::Visible - # Respect the collapsed state of categories (indicated by + prefix) - if ($categoryLabel.Content -like "+*") { - $wrapPanel.Visibility = [Windows.Visibility]::Collapsed - } else { - $wrapPanel.Visibility = [Windows.Visibility]::Visible - } - - # Show all apps within the category + # Search through apps in this category $wrapPanel.Children | ForEach-Object { - $_.Visibility = [Windows.Visibility]::Visible + # Safely retrieve app entry from hashtable + $appTag = $_.Tag + $appEntry = $null + + if (-not [string]::IsNullOrWhiteSpace($appTag) -and $sync.configs.applicationsHashtable.ContainsKey($appTag)) { + $appEntry = $sync.configs.applicationsHashtable[$appTag] + } + + # Check if app matches search criteria + if ($null -ne $appEntry) { + $contentMatch = $appEntry.Content -like "*$escapedSearchString*" + $descriptionMatch = $appEntry.Description -like "*$escapedSearchString*" + + if ($contentMatch -or $descriptionMatch) { + # Show the App and mark that this category has a match + $_.Visibility = [Windows.Visibility]::Visible + $categoryHasMatch = $true + } + else { + $_.Visibility = [Windows.Visibility]::Collapsed + } + } + else { + # Hide app if no entry found (data integrity issue) + $_.Visibility = [Windows.Visibility]::Collapsed + } } - } - } - return - } - # Perform search - $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 - - # Keep category label visible - $categoryLabel.Visibility = [Windows.Visibility]::Visible - - # Search through apps in this category - $wrapPanel.Children | ForEach-Object { - $appEntry = $sync.configs.applicationsHashtable.$($_.Tag) - if ($appEntry.Content -like "*$SearchString*" -or $appEntry.Description -like "*$SearchString*") { - # Show the App and mark that this category has a match + # If category has matches, show the WrapPanel and update the category label to expanded state + if ($categoryHasMatch) { + $wrapPanel.Visibility = [Windows.Visibility]::Visible $_.Visibility = [Windows.Visibility]::Visible - $categoryHasMatch = $true + # Update category label to show expanded state (-) + if ($categoryLabel.Content -like "+*") { + $categoryLabel.Content = $categoryLabel.Content -replace "^\+ ", "- " + } } else { + # Hide the entire category container if no matches $_.Visibility = [Windows.Visibility]::Collapsed } } - - # If category has matches, show the WrapPanel and update the category label to expanded state - if ($categoryHasMatch) { - $wrapPanel.Visibility = [Windows.Visibility]::Visible - $_.Visibility = [Windows.Visibility]::Visible - # Update category label to show expanded state (-) - if ($categoryLabel.Content -like "+*") { - $categoryLabel.Content = $categoryLabel.Content -replace "^\+ ", "- " - } - } else { - # Hide the entire category container if no matches - $_.Visibility = [Windows.Visibility]::Collapsed - } } } + catch { + Write-Warning "Find-AppsByNameOrDescription: An error occurred during search: $_" + # Fail gracefully - do not crash the UI thread + return + } } diff --git a/functions/private/Find-TweaksByNameOrDescription.ps1 b/functions/private/Find-TweaksByNameOrDescription.ps1 index 81e9cb39..497c33d7 100644 --- a/functions/private/Find-TweaksByNameOrDescription.ps1 +++ b/functions/private/Find-TweaksByNameOrDescription.ps1 @@ -3,98 +3,300 @@ 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 + 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) #> param( - [Parameter(Mandatory=$false)] + [Parameter(Mandatory = $false)] [string]$SearchString = "" ) - # Reset the visibility if the search string is empty or the search is cleared - if ([string]::IsNullOrWhiteSpace($SearchString)) { - # Show all categories - $tweakspanel = $sync.Form.FindName("tweakspanel") - $tweakspanel.Children | ForEach-Object { - $_.Visibility = [Windows.Visibility]::Visible + # ────────────────────────────────────────────────────────────────────────────── + # 1. RESOLVE $SYNC WITH MULTI-LEVEL FALLBACK + # ────────────────────────────────────────────────────────────────────────────── - # Foreach category section, show all items - if ($_ -is [Windows.Controls.Border]) { - $_.Visibility = [Windows.Visibility]::Visible - - # Find ItemsControl - $dockPanel = $_.Child - if ($dockPanel -is [Windows.Controls.DockPanel]) { - $itemsControl = $dockPanel.Children | Where-Object { $_ -is [Windows.Controls.ItemsControl] } - if ($itemsControl) { - # Show items in the category - foreach ($item in $itemsControl.Items) { - if ($item -is [Windows.Controls.Label]) { - $item.Visibility = [Windows.Visibility]::Visible - } elseif ($item -is [Windows.Controls.DockPanel] -or - $item -is [Windows.Controls.StackPanel]) { - $item.Visibility = [Windows.Visibility]::Visible - } - } - } - } - } + 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 } - # Search for matching tweaks when search string is not null - $tweakspanel = $sync.Form.FindName("tweakspanel") + if ($null -eq $Sync.Form) { + # Silent return - form not yet initialized + return + } - $tweakspanel.Children | ForEach-Object { - $categoryBorder = $_ - $categoryVisible = $false + # ────────────────────────────────────────────────────────────────────────────── + # 2. GET REFERENCE TO TWEAKS PANEL + # ────────────────────────────────────────────────────────────────────────────── - if ($_ -is [Windows.Controls.Border]) { - # Find the ItemsControl - $dockPanel = $_.Child - if ($dockPanel -is [Windows.Controls.DockPanel]) { - $itemsControl = $dockPanel.Children | Where-Object { $_ -is [Windows.Controls.ItemsControl] } - if ($itemsControl) { - $categoryLabel = $null + $tweaksPanel = $null + try { + $tweaksPanel = $Sync.Form.FindName("tweakspanel") + } + catch { + # Silent return - panel not found or disposed + return + } - # Process all items in the ItemsControl - for ($i = 0; $i -lt $itemsControl.Items.Count; $i++) { - $item = $itemsControl.Items[$i] + if ($null -eq $tweaksPanel) { + # Silent return - panel doesn't exist + return + } - if ($item -is [Windows.Controls.Label]) { - $categoryLabel = $item - $item.Visibility = [Windows.Visibility]::Collapsed - } elseif ($item -is [Windows.Controls.DockPanel]) { - $checkbox = $item.Children | Where-Object { $_ -is [Windows.Controls.CheckBox] } | Select-Object -First 1 - $label = $item.Children | Where-Object { $_ -is [Windows.Controls.Label] } | Select-Object -First 1 + # ────────────────────────────────────────────────────────────────────────────── + # 3. HANDLE EMPTY/WHITESPACE SEARCH STRING - RESET TO DEFAULT STATE + # ────────────────────────────────────────────────────────────────────────────── - if ($label -and ($label.Content -like "*$SearchString*" -or $label.ToolTip -like "*$SearchString*")) { - $item.Visibility = [Windows.Visibility]::Visible - if ($categoryLabel) { $categoryLabel.Visibility = [Windows.Visibility]::Visible } - $categoryVisible = $true - } else { - $item.Visibility = [Windows.Visibility]::Collapsed - } - } elseif ($item -is [Windows.Controls.StackPanel]) { - # StackPanel which contain checkboxes or other elements - $checkbox = $item.Children | Where-Object { $_ -is [Windows.Controls.CheckBox] } | Select-Object -First 1 + if ([string]::IsNullOrWhiteSpace($SearchString)) { + try { + $tweaksPanel.Children | ForEach-Object { + $categoryBorder = $_ - if ($checkbox -and ($checkbox.Content -like "*$SearchString*" -or $checkbox.ToolTip -like "*$SearchString*")) { - $item.Visibility = [Windows.Visibility]::Visible - if ($categoryLabel) { $categoryLabel.Visibility = [Windows.Visibility]::Visible } - $categoryVisible = $true - } else { - $item.Visibility = [Windows.Visibility]::Collapsed + # 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]) { + $itemsControl = $null + $itemsControl = $dockPanel.Children | Where-Object { $_ -is [Windows.Controls.ItemsControl] } | Select-Object -First 1 + + if ($null -ne $itemsControl) { + # Show all items in the category + foreach ($item in $itemsControl.Items) { + if ($null -ne $item) { + # Check if it's a category label (first Label in the ItemsControl) + if ($item -is [Windows.Controls.Label]) { + $item.Visibility = [Windows.Visibility]::Visible + } + elseif ($item -is [Windows.Controls.DockPanel] -or $item -is [Windows.Controls.StackPanel]) { + # Show all checkbox containers + $item.Visibility = [Windows.Visibility]::Visible + } + } } } } } } + } + catch { + # Silent catch - UI element may be disposed + } - # Set the visibility based on if any item matched - $categoryBorder.Visibility = if ($categoryVisible) { [Windows.Visibility]::Visible } else { [Windows.Visibility]::Collapsed } + return + } + # ────────────────────────────────────────────────────────────────────────────── + # 4. PERFORM LITERAL SEARCH (NO WILDCARD EXPANSION) + # ────────────────────────────────────────────────────────────────────────────── + + try { + # Normalize search term once for the entire operation + $searchTerm = $SearchString + if ($null -eq $searchTerm) { + $searchTerm = "" + } + + # Iterate through all categories + $tweaksPanel.Children | ForEach-Object { + $categoryBorder = $_ + $categoryHasMatch = $false + + if ($categoryBorder -is [Windows.Controls.Border]) { + $dockPanel = $null + if ($null -ne $categoryBorder.Child) { + $dockPanel = $categoryBorder.Child + } + + if ($dockPanel -is [Windows.Controls.DockPanel]) { + $itemsControl = $null + $itemsControl = $dockPanel.Children | Where-Object { $_ -is [Windows.Controls.ItemsControl] } | Select-Object -First 1 + + if ($null -ne $itemsControl) { + $categoryLabel = $null + + # Process all items (checkboxes, labels, panels) in the ItemsControl + for ($i = 0; $i -lt $itemsControl.Items.Count; $i++) { + $item = $itemsControl.Items[$i] + + if ($null -eq $item) { + continue + } + + # ──────────────────────────────────────────────────────────── + # Check if this is a category label (usually first Label) + # ──────────────────────────────────────────────────────────── + + if ($item -is [Windows.Controls.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]) { + $checkbox = $null + $label = $null + + # Safely extract checkbox and label + $checkbox = $item.Children | Where-Object { $_ -is [Windows.Controls.CheckBox] } | Select-Object -First 1 + $label = $item.Children | Where-Object { $_ -is [Windows.Controls.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]) { + $checkbox = $null + $checkbox = $item.Children | Where-Object { $_ -is [Windows.Controls.CheckBox] } | Select-Object -First 1 + + $itemMatches = $false + + if ($null -ne $checkbox) { + $checkboxContent = $checkbox.Content + $checkboxToolTip = $checkbox.ToolTip + + # 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 + + # Safe string replacement without -replace regex + if ($labelStr.StartsWith("+ ")) { + $expandedLabel = "- " + $labelStr.Substring(2) + $categoryLabel.Content = $expandedLabel + } + } + } + } + } + } + + # ──────────────────────────────────────────────────────────────── + # Set category border visibility based on whether it has matches + # ──────────────────────────────────────────────────────────────── + + if ($categoryHasMatch) { + $categoryBorder.Visibility = [Windows.Visibility]::Visible + } + else { + $categoryBorder.Visibility = [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 + } }