Private/Interactive/Show-TBMenuArrowAccordion.ps1
|
function Build-TBAccordionRows { <# .SYNOPSIS Builds a flat list of visible rows from accordion section definitions. .DESCRIPTION Computes the visible parent and child rows based on which section (if any) is currently expanded. Returns an array of hashtables with Type, Label, SectionIndex, ChildIndex, ChildCount, Expanded, and IsDirect fields. .PARAMETER Sections Array of section hashtables. Each must have Title (string), Children (string array), and IsDirect (bool). .PARAMETER ExpandedIndex The index of the currently expanded section, or -1 for none. #> [CmdletBinding()] [OutputType([object[]])] param( [Parameter(Mandatory = $true)] [object[]]$Sections, [Parameter()] [int]$ExpandedIndex = -1 ) $rows = [System.Collections.ArrayList]::new() for ($s = 0; $s -lt $Sections.Count; $s++) { $section = $Sections[$s] $isExpanded = ($s -eq $ExpandedIndex) -and ($section.Children.Count -gt 0) $null = $rows.Add(@{ Type = 'parent' SectionIndex = $s Label = $section.Title ChildCount = $section.Children.Count Expanded = $isExpanded IsDirect = $section.IsDirect }) if ($isExpanded) { for ($c = 0; $c -lt $section.Children.Count; $c++) { $null = $rows.Add(@{ Type = 'child' SectionIndex = $s ChildIndex = $c Label = $section.Children[$c] }) } } } return @(, $rows.ToArray()) } function Show-TBMenuArrowAccordion { <# .SYNOPSIS Arrow-key accordion menu navigator for PS 7+ interactive terminals. .DESCRIPTION Renders an accordion-style menu where parent sections expand/collapse inline to reveal child actions. Only one section is expanded at a time. Returns a hashtable with Section and Item indices, or the string 'Quit'. .PARAMETER Sections Array of section hashtables. Each must have Title (string), Children (string array), and IsDirect (bool). IsDirect sections execute immediately on Enter without expanding children. .PARAMETER InitialExpanded Index of the section to start expanded, or -1 for none. #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [object[]]$Sections, [Parameter()] [int]$InitialExpanded = -1 ) $palette = Get-TBColorPalette $esc = [char]27 $reset = "${esc}[0m" # Print accordion title inside the box continuation $innerWidth = Get-TBConsoleInnerWidth $border = ([char]0x2502) $hLine = ([char]0x2500) $titleText = ' Main Menu' $titlePadded = $titleText.PadRight($innerWidth) Write-Host (' {0}{1}{2}{3}{4}{5}{6}' -f $palette.Surface, $border, $palette.Teal, $palette.Bold, $titlePadded, $reset, ('{0}{1}{2}' -f $palette.Surface, $border, $reset)) $titleUnderline = (' ' + ([string]$hLine * 9)).PadRight($innerWidth) Write-Host (' {0}{1}{2}{3}{4}{5}' -f $palette.Surface, $border, $palette.Dim, $titleUnderline, $reset, ('{0}{1}{2}' -f $palette.Surface, $border, $reset)) # Record anchor position $anchorTop = [Console]::CursorTop $expandedIndex = $InitialExpanded $selectedIndex = 0 $previousRowCount = 0 # If we have an initial expanded section, position cursor on first child if ($expandedIndex -ge 0 -and $expandedIndex -lt $Sections.Count) { if ($Sections[$expandedIndex].Children.Count -gt 0) { # Count rows up to the expanded section's first child $offset = $expandedIndex + 1 # parent rows before + the expanded parent itself $selectedIndex = $offset } else { $selectedIndex = $expandedIndex } } $rows = Build-TBAccordionRows -Sections $Sections -ExpandedIndex $expandedIndex try { try { [Console]::CursorVisible = $false } catch { } # Initial render Render-TBAccordionBox -Rows $rows -SelectedIndex $selectedIndex -AnchorTop $anchorTop -PreviousRowCount $previousRowCount $previousRowCount = $rows.Count while ($true) { $key = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown') $itemCount = $rows.Count switch ($key.VirtualKeyCode) { 38 { # Up arrow if ($selectedIndex -gt 0) { $selectedIndex-- } else { $selectedIndex = $itemCount - 1 } Render-TBAccordionBox -Rows $rows -SelectedIndex $selectedIndex -AnchorTop $anchorTop -PreviousRowCount $previousRowCount $previousRowCount = $rows.Count } 40 { # Down arrow if ($selectedIndex -lt ($itemCount - 1)) { $selectedIndex++ } else { $selectedIndex = 0 } Render-TBAccordionBox -Rows $rows -SelectedIndex $selectedIndex -AnchorTop $anchorTop -PreviousRowCount $previousRowCount $previousRowCount = $rows.Count } 39 { # Right arrow - expand current section $currentRow = $rows[$selectedIndex] if ($currentRow.Type -eq 'parent' -and -not $currentRow.Expanded -and -not $currentRow.IsDirect -and $Sections[$currentRow.SectionIndex].Children.Count -gt 0) { $expandedIndex = $currentRow.SectionIndex $rows = Build-TBAccordionRows -Sections $Sections -ExpandedIndex $expandedIndex # Move selection to first child for ($f = 0; $f -lt $rows.Count; $f++) { if ($rows[$f].Type -eq 'child' -and $rows[$f].SectionIndex -eq $expandedIndex) { $selectedIndex = $f break } } Render-TBAccordionBox -Rows $rows -SelectedIndex $selectedIndex -AnchorTop $anchorTop -PreviousRowCount $previousRowCount $previousRowCount = $rows.Count } } 37 { # Left arrow - collapse current expanded section if ($expandedIndex -ge 0) { $collapseTarget = $expandedIndex $expandedIndex = -1 $rows = Build-TBAccordionRows -Sections $Sections -ExpandedIndex $expandedIndex # Move selection to the collapsed parent $selectedIndex = $collapseTarget if ($selectedIndex -ge $rows.Count) { $selectedIndex = $rows.Count - 1 } Render-TBAccordionBox -Rows $rows -SelectedIndex $selectedIndex -AnchorTop $anchorTop -PreviousRowCount $previousRowCount $previousRowCount = $rows.Count } } 13 { # Enter $currentRow = $rows[$selectedIndex] if ($currentRow.Type -eq 'child') { # Return child selection return @{ Section = $currentRow.SectionIndex Item = $currentRow.ChildIndex } } elseif ($currentRow.IsDirect) { # Direct-action parent (no children) return @{ Section = $currentRow.SectionIndex Item = -1 } } else { # Toggle expand/collapse on parent if ($currentRow.Expanded) { $expandedIndex = -1 } else { $expandedIndex = $currentRow.SectionIndex } $rows = Build-TBAccordionRows -Sections $Sections -ExpandedIndex $expandedIndex # If expanding, move to first child; if collapsing, stay on parent if ($expandedIndex -ge 0 -and $Sections[$expandedIndex].Children.Count -gt 0) { for ($f = 0; $f -lt $rows.Count; $f++) { if ($rows[$f].Type -eq 'child' -and $rows[$f].SectionIndex -eq $expandedIndex) { $selectedIndex = $f break } } } else { # Find the parent row for the collapsed section for ($f = 0; $f -lt $rows.Count; $f++) { if ($rows[$f].Type -eq 'parent' -and $rows[$f].SectionIndex -eq $currentRow.SectionIndex) { $selectedIndex = $f break } } } Render-TBAccordionBox -Rows $rows -SelectedIndex $selectedIndex -AnchorTop $anchorTop -PreviousRowCount $previousRowCount $previousRowCount = $rows.Count } } 27 { # Escape return 'Quit' } } } } finally { try { [Console]::CursorVisible = $true } catch { } } } |