ModuleExplorer.psm1

<#
.SYNOPSIS
    Displays an interactive viewer for commands (cmdlets, functions, aliases)
    within a selected PowerShell module.
 
.DESCRIPTION
    Show-ModuleCommandViewer is a private function within the ModuleExplorer module.
    It provides a text-based user interface (TUI) for browsing
    and inspecting commands of a specified PowerShell module.
 
    The interface is divided into two main panes:
 
    Left Pane: Lists all exported commands from the selected module. Commands are
    color-coded by type (Cmdlet, Function, Alias). Users can navigate this list
    using arrow keys and filter it by typing.
 
    Right Pane: Initially displays the synopsis of the selected command. After
    selecting a command (Right Arrow or Enter), it shows help options
    (Examples, Detailed, Full, Online). Selecting a help option displays
    the corresponding help content, which can be scrolled.
 
    Navigation is primarily through arrow keys, Enter, and Escape. Instructions
    for key bindings are displayed at the bottom of the viewer.
 
    This function utilizes the PwshSpectreConsole module to render its UI.
 
.PARAMETER SelectedModule
    A PSObject representing the PowerShell module whose commands are to be displayed.
    This object MUST have a 'Name' property containing the string name of the module
    (e.g., as returned by Get-Module). This parameter is mandatory. It is provided
    by the caller of this function, the Show-ModuleExplorer function.
 
.INPUTS
    System.Management.Automation.PSObject
    Expects a PSObject with a .Name property that is the name of a module.
 
.OUTPUTS
    None.
    This function does not return any objects to the pipeline. Its output is entirely
    directed to the host console as an interactive UI. It returns $null upon exiting.
 
.NOTES
    This is a private function and is not intended to be called directly by users
    of the ModuleExplorer module. It is used internally to provide UI capabilities.
     
    Requires the Spectre.Console PowerShell module to be available.
    The function attempts to dynamically adjust the displayed list sizes based on
    console window height. This works best when expanding the window; shrinking
    may not immediately reflect without reselecting the module.
 
    Search functionality in the command list is case-insensitive and supports
    alphanumeric characters, hyphens, and underscores.
 
    Key Interactions:
    - Command List (Left Pane - Initial View):
        - Up/Down Arrows: Navigate command list.
        - Type characters: Filter the command list.
        - Backspace/Left Arrow (when search string active): Delete last character from search.
        - Right Arrow / Enter: Select command and move to Help Options view.
        - Left Arrow (no search string): Go back (effectively exits in this initial context if called from a parent menu).
        - Escape: Exit the viewer.
 
    - Help Options (Right Pane - After selecting a command):
        - Up/Down Arrows: Navigate help options (Examples, Detailed, Full, Online).
        - Right Arrow / Enter: View selected help content.
        - Left Arrow: Return to Command List (Description view).
        - Escape: Exit the viewer.
 
    - Help Content (Right Pane - After selecting a help option):
        - Up/Down Arrows: Scroll through help content.
        - Right Arrow / Enter (if "Online" help was selected and is pending): Open online help in browser.
        - Left Arrow: Return to Help Options view.
        - Escape: Exit the viewer.
 
.EXAMPLE
    # This is a private function. The following shows conceptual internal usage:
 
    $moduleObject = Get-Module -Name "Pester" -ListAvailable | Select-Object -First 1
    if ($moduleObject) {
        Show-ModuleCommandViewer -SelectedModule $moduleObject
    }
    # This would launch the interactive command viewer for the 'Pester' module.
 
.LINK
    None
#>

function Show-ModuleCommandViewer {
    param (
        [Parameter(Mandatory)]
        [PSObject]$SelectedModule
    )

    $commands = Get-Command -Module $SelectedModule.Name | Sort-Object CommandType, Name
    
    if (-not $commands) {
        Write-SpectreHost "[yellow]No exported commands found for module '$($SelectedModule.Name)'.[/]"
        Read-SpectrePause -Message "[grey]Press Enter to continue...[/]" -NoNewline
        return
    }

    # Pre-fetch command details, including synopsis
    $allCommandObjects = @($commands | ForEach-Object {
        $synopsis = ""
        try {
            $helpInfo = Get-Help $_.Name -ErrorAction SilentlyContinue
            if ($helpInfo) {
                if ($helpInfo.Synopsis -is [array]) {
                    $synopsis = ($helpInfo.Synopsis | Select-Object -First 1) -join " "
                } elseif ($helpInfo.Synopsis) {
                    $synopsis = $helpInfo.Synopsis
                }
            }
        } catch {
            Write-Verbose "Failed to get help for command '$($_.Name)': $($_.Exception.Message)"
            # Falls back to empty synopsis string
        }
        
        [PSCustomObject]@{
            Name        = $_.Name
            Type        = $_.CommandType.ToString()
            Source      = $_.Source
            Definition  = $_.Definition
            Synopsis    = $synopsis
            CommandInfo = $_ # Store the OG object
        }
    })

    if (-not $allCommandObjects) {
        Write-SpectreHost "[yellow]Could not retrieve command details for '$($SelectedModule.Name)'.[/]"
        Read-SpectrePause -Message "[grey]Press Enter to continue...[/]" -NoNewline
        return
    }
    
    # Interactive Help Viewer with Invoke-SpectreLive
    # Ratios are king, not sure I can resize without them
    $initialCommandListContent = Write-SpectreHost "[grey]Loading command list...[/]" -PassThru | Format-SpectrePanel -Header "[bold]Commands[/]" -Expand -Border Rounded
    $initialRightPanelContent = Write-SpectreHost "[grey]Select a command to see description.[/]" -PassThru | Format-SpectrePanel -Header "[bold]Description[/]" -Expand -Border Rounded

    $commandListPaneLayout = New-SpectreLayout -Name "commandListPane" -Data $initialCommandListContent -Ratio 1
    $rightPaneLayout = New-SpectreLayout -Name "rightPane" -Data $initialRightPanelContent -Ratio 3

    $combinedPanel = New-SpectreLayout -Name "combinedPanel" -Columns @($commandListPaneLayout , $rightPaneLayout) -Ratio 10

    $titleRenderable = Write-SpectreHost "`n[green bold]Cmdlets[/], [blue bold]Functions[/], and [magenta bold]Aliases[/] in $($SelectedModule.Name)" -PassThru | Format-SpectrePadded -Top 0 -Right 0 -Bottom 0 -Left 1
    $instructionsText = "[grey](↑/↓ Navigate | → Select | ← Back | Type to Search | Esc Exit)[/]"
    $instructionsRenderable = Write-SpectreHost $instructionsText -PassThru | Format-SpectrePadded -Top 1 -Right 0 -Bottom 0 -Left 0 | Format-SpectreAligned -HorizontalAlignment Center
    $layout = New-SpectreLayout -Name "root" -Rows @($titleRenderable, $combinedPanel, $instructionsRenderable)

    Invoke-SpectreLive -Data $layout -ScriptBlock {
        param (
            [Spectre.Console.LiveDisplayContext] $LiveContext
        )
        
        # Set default variables for the live UI
        $currentCommandIndex = 0
        $currentHelpOptionIndex = 0
        $rightPaneView = 'Description'
        
        $searchString = ""
        $filteredCommandObjects = $allCommandObjects
        $currentCommandObjectForHelp = $null

        # Estimate for fixed rows header, footer, borders, instrucitons
        $fixedRowsOverhead = 5
        $dynamicPageSize = ($Host.UI.RawUI.WindowSize.Height - $fixedRowsOverhead)
        if ($dynamicPageSize -lt 1) {$dynamicPageSize = 1}
        
        $commandListPageSize = $dynamicPageSize
        $commandListScrollOffset = 0

        $helpOptions = @("Examples", "Detailed", "Full", "Online" )
        $currentHelpContentLines = @()
        $helpContentScrollOffset = 0
        $helpContentPageSize = $dynamicPageSize


        try {
            while ($true) {
                # Recalculate dynamic page sizes in case console was resized
                # This doesn't work when shrinking the console, but it does when expanding
                # Will need to revisit to see if I can resolve that issue
                $newDynamicPageSize = ($Host.UI.RawUI.WindowSize.Height - $fixedRowsOverhead)
                if ($newDynamicPageSize -lt 1) {$newDynamicPageSize = 1}

                if ($newDynamicPageSize -ne $dynamicPageSize) {
                    $dynamicPageSize = $newDynamicPageSize
                    $commandListPageSize = $dynamicPageSize
                    $helpContentPageSize = $dynamicPageSize
                    
                    # Re-clamp scroll offsets if page size changes
                    $commandListTotalItemsForClamp = $filteredCommandObjects.Count # Use current filtered count
                    if ($commandListTotalItemsForClamp -gt 0) {
                        $commandListScrollOffset = [System.Math]::Min($commandListScrollOffset, [System.Math]::Max(0, $commandListTotalItemsForClamp - $commandListPageSize))
                    } else {
                        $commandListScrollOffset = 0
                    }
                    if ($currentHelpContentLines.Count -gt 0) {
                        $helpContentScrollOffset = [System.Math]::Min($helpContentScrollOffset, [System.Math]::Max(0, $currentHelpContentLines.Count - $helpContentPageSize))
                    } else {
                        $helpContentScrollOffset = 0
                    }
                }

                # Filter commands based on search string
                if ($searchString -ne "") {
                    $filteredCommandObjects = $allCommandObjects | Where-Object { $_.Name -like "*$searchString*" }
                    if ($currentCommandIndex -ge $filteredCommandObjects.Count -and $filteredCommandObjects.Count -gt 0) {
                        $currentCommandIndex = $filteredCommandObjects.Count - 1 # Select last item
                    } elseif ($filteredCommandObjects.Count -eq 0) {
                        $currentCommandIndex = -1 # No item selected
                    }
                    # Reset scroll if filter changes and current index is off-page or invalid
                    if ($currentCommandIndex -eq -1 -or $currentCommandIndex -lt $commandListScrollOffset -or $currentCommandIndex -ge ($commandListScrollOffset + $commandListPageSize)) {
                        $commandListScrollOffset = [System.Math]::Max(0, $currentCommandIndex - [System.Math]::Floor($commandListPageSize / 2))
                        if ($commandListTotalItems -gt 0) {
                            $commandListScrollOffset = [System.Math]::Min($commandListScrollOffset, [System.Math]::Max(0, $commandListTotalItems - $commandListPageSize))
                        } else {
                            $commandListScrollOffset = 0
                        }
                    }

                } else {
                    $filteredCommandObjects = $allCommandObjects
                }
                $commandListTotalItems = $filteredCommandObjects.Count

                if ($commandListTotalItems -eq 0) {
                    $currentCommandIndex = -1
                    $commandListScrollOffset = 0
                } elseif ($currentCommandIndex -eq -1 -or $currentCommandIndex -ge $commandListTotalItems) {
                    # If previous selection is now invalid ( after clearing search)
                    $currentCommandIndex = 0
                    $commandListScrollOffset = 0
                }


                # Command List Panel (Left Pane)
                $listItems = New-Object System.Collections.Generic.List[string]
                $commandListPanelHeader = "[bold]($($commandListTotalItems) total)[/]"
                if ($searchString -ne "") {
                    $commandListPanelHeader += " Filter: [yellow]'$($searchString)'[/]"
                }

                if ($commandListTotalItems -gt 0 -and $currentCommandIndex -ne -1) {
                    if ($commandListScrollOffset -gt 0) { $listItems.Add("[grey] ↑ ...[/]")}

                    $visibleListStartIndex = $commandListScrollOffset
                    $visibleListEndIndex = [System.Math]::Min(($commandListScrollOffset + $commandListPageSize - 1), ($commandListTotalItems - 1))

                    for ($i = $visibleListStartIndex; $i -le $visibleListEndIndex; $i++) {
                        if ($i -lt 0 -or $i -ge $filteredCommandObjects.Count) { continue }
                        $cmd = $filteredCommandObjects[$i]
                        $displayName = $cmd.Name
                        $styledName = switch ($cmd.Type) {
                            'Cmdlet'   { "[green]$displayName[/]" }
                            'Function' { "[blue]$displayName[/]" }
                            'Alias'    { "[magenta]$displayName[/]" }
                            default    { $displayName }
                        }
                        if ($i -eq $currentCommandIndex) { $listItems.Add("[yellow bold]>[/] $($styledName)") }
                        else { $listItems.Add(" $($styledName)") }
                    }
                    if ($visibleListEndIndex -lt ($commandListTotalItems - 1)) { $listItems.Add("[grey] ↓ ...[/]")}

                } else {
                    $listItems.Add("[grey] (No commands to display) [/]")
                }
            
                $commandListPanel = $listItems | Format-SpectreRows | Format-SpectrePanel -Header $commandListPanelHeader -Expand -Border Rounded
                $layout["commandListPane"].Update($commandListPanel) | Out-Null
                

                # Right Pane for Command View (Description, Help Options, or Help Content)
                $rightPanelContentRenderable = $null
                $rightPanelHeader = "[bold]Info[/]"

                if ($rightPaneView -eq 'Description') {
                    $rightPanelHeader = "[bold]Description[/]"
                    if ($currentCommandIndex -ge 0 -and $currentCommandIndex -lt $filteredCommandObjects.Count) {
                        $currentCmdForDesc = $filteredCommandObjects[$currentCommandIndex]
                        $rightPanelHeader = "[bold]Description for $($currentCmdForDesc.Name)[/]"
                        $descriptionText = if ($currentCmdForDesc.Synopsis -and $currentCmdForDesc.Synopsis -ne "") {
                            $currentCmdForDesc.Synopsis
                        } elseif ($currentCmdForDesc.Type -eq 'Alias' -and $currentCmdForDesc.Definition) {
                            "Alias for: $($currentCmdForDesc.Definition)"
                        } else { "No synopsis available." }
                        $rightPanelContentRenderable = ($descriptionText | Get-SpectreEscapedText | Format-SpectrePanel -Header $rightPanelHeader -Expand -Border Rounded)
                    } else {
                        $rightPanelContentRenderable = (Write-SpectreHost "[grey]No command selected or found.[/]" -PassThru | Format-SpectrePanel -Header $rightPanelHeader -Expand -Border Rounded)
                    }
                } elseif ($rightPaneView -eq 'HelpOptions') {
                    $rightPanelHeader = "[bold]Help Options for $($currentCommandObjectForHelp.Name)[/]"
                    $helpOptionListItems = for ($i = 0; $i -lt $helpOptions.Count; $i++) {
                        if ($i -eq $currentHelpOptionIndex) { "[yellow bold]>[/] $($helpOptions[$i])" }
                        else { " $($helpOptions[$i])" }
                    }
                    $rightPanelContentRenderable = ($helpOptionListItems | Format-SpectreRows | Format-SpectrePanel -Header $rightPanelHeader -Expand -Border Rounded)
                } elseif ($rightPaneView -eq 'HelpContent') {
                    $rightPanelHeader = "[bold]Help: $($currentCommandObjectForHelp.Name) - $($helpOptions[$currentHelpOptionIndex])[/]"
                    
                    $visibleHelpLines = New-Object System.Collections.Generic.List[string]
                    if ($currentHelpContentLines.Count -gt 0) {
                        if ($helpContentScrollOffset -gt 0) { $visibleHelpLines.Add("[grey] ↑ ...[/]")}
                        
                        $helpViewEndIndex = [System.Math]::Min(($helpContentScrollOffset + $helpContentPageSize - 1), ($currentHelpContentLines.Count - 1))
                        for ($l = $helpContentScrollOffset; $l -le $helpViewEndIndex; $l++) {
                            if ($l -ge 0 -and $l -lt $currentHelpContentLines.Count) {
                                $visibleHelpLines.Add($currentHelpContentLines[$l])
                            }
                        }

                        if ($helpViewEndIndex -lt ($currentHelpContentLines.Count - 1)) { $visibleHelpLines.Add("[grey] ↓ ...[/]")}
                    } else {
                        $visibleHelpLines.Add($currentHelpContent)
                    }
                    $rightPanelContentRenderable = ($visibleHelpLines | Format-SpectreRows | Format-SpectrePanel -Header $rightPanelHeader -Expand -Border Rounded)
                }
                
                $layout["rightPane"].Update($rightPanelContentRenderable) | Out-Null
            
                $LiveContext.Refresh()

                # Input handling
                if (-not [Console]::KeyAvailable) { Start-Sleep -Milliseconds 50; continue }
                $keyInfo = [Console]::ReadKey($true)
                
                if ($keyInfo.Key -eq [System.ConsoleKey]::Escape) { return $null }

                # Type to Search but only when in Description view
                if ($rightPaneView -eq 'Description' -and
                    (($keyInfo.KeyChar -ge 'a' -and $keyInfo.KeyChar -le 'z') -or
                    ($keyInfo.KeyChar -ge 'A' -and $keyInfo.KeyChar -le 'Z') -or
                    ($keyInfo.KeyChar -ge '0' -and $keyInfo.KeyChar -le '9') -or
                    $keyInfo.KeyChar -eq '-' -or $keyInfo.KeyChar -eq '_' ) ) {
                    $searchString += $keyInfo.KeyChar
                    $currentCommandIndex = 0
                    $commandListScrollOffset = 0
                    continue
                }
                
                # More Input Handling!
                if ($rightPaneView -eq 'Description') {
                    switch ($keyInfo.Key) {
                        ([System.ConsoleKey]::UpArrow) {
                            if ($currentCommandIndex -gt 0) {
                                $currentCommandIndex--
                                if ($currentCommandIndex -lt $commandListScrollOffset) {
                                    $commandListScrollOffset = $currentCommandIndex # Snap to top
                                }
                            }
                        }
                        ([System.ConsoleKey]::DownArrow) {
                            if ($commandListTotalItems -gt 0 -and $currentCommandIndex -lt ($commandListTotalItems - 1)) {
                                $currentCommandIndex++
                                if ($currentCommandIndex -ge ($commandListScrollOffset + $commandListPageSize)) {
                                    $commandListScrollOffset++ # Scroll down one line
                                }
                            }
                        }
                        ([System.ConsoleKey]::RightArrow) {
                            if ($currentCommandIndex -ge 0 -and $currentCommandIndex -lt $filteredCommandObjects.Count) {
                                $currentCommandObjectForHelp = $filteredCommandObjects[$currentCommandIndex]
                                $rightPaneView = 'HelpOptions'
                                $currentHelpOptionIndex = 0
                                $searchString = ""
                            }
                        }
                        ([System.ConsoleKey]::LeftArrow) {
                            if ($searchString.Length -gt 0) {
                                $searchString = $searchString.Substring(0, $searchString.Length - 1)
                                $currentCommandIndex = 0
                                $commandListScrollOffset = 0
                            } else {
                                return $null
                            }
                        }
                        ([System.ConsoleKey]::Backspace) {
                            if ($searchString.Length -gt 0) {
                                $searchString = $searchString.Substring(0, $searchString.Length - 1)
                                $currentCommandIndex = 0
                                $commandListScrollOffset = 0
                            }
                        }
                        # This is just a mirror of the right arrow
                        ([System.ConsoleKey]::Enter) {
                            if ($currentCommandIndex -ge 0 -and $currentCommandIndex -lt $filteredCommandObjects.Count) {
                                $currentCommandObjectForHelp = $filteredCommandObjects[$currentCommandIndex]
                                $rightPaneView = 'HelpOptions'
                                $currentHelpOptionIndex = 0
                                $searchString = ""
                            }
                        }
                    }
                } elseif ($rightPaneView -eq 'HelpOptions') {
                    switch ($keyInfo.Key) {
                        ([System.ConsoleKey]::UpArrow) {
                            if ($currentHelpOptionIndex -gt 0) { $currentHelpOptionIndex-- }
                        }
                        ([System.ConsoleKey]::DownArrow) {
                            if ($currentHelpOptionIndex -lt ($helpOptions.Count - 1)) { $currentHelpOptionIndex++ }
                        }
                        ([System.ConsoleKey]::RightArrow) {
                            $selectedHelpType = $helpOptions[$currentHelpOptionIndex]
                            $currentHelpContent = "[grey]Fetching help...[/]"
                            $currentHelpContentLines = @()
                            $helpContentScrollOffset = 0
                            $rightPaneView = 'HelpContent'
                            $LiveContext.Refresh()

                            if ($selectedHelpType -eq "Online") {
                                $currentHelpContent = "[yellow]Press Right Arrow or Enter to open online help (if available), or Left Arrow to go back.[/]"
                                $currentHelpContentLines = @($currentHelpContent.ToString())
                            } else {
                                try {
                                    $helpText = ""
                                    switch($selectedHelpType) {
                                        # "Synopsis" { $helpText = Get-Help $currentCommandObjectForHelp.CommandInfo | Out-String }
                                        "Examples" { $helpText = Get-Help $currentCommandObjectForHelp.CommandInfo -Examples | Out-String }
                                        "Detailed" { $helpText = Get-Help $currentCommandObjectForHelp.CommandInfo -Detailed | Out-String }
                                        "Full"     { $helpText = Get-Help $currentCommandObjectForHelp.CommandInfo -Full | Out-String }
                                    }
                                    $currentHelpContentLines = ($helpText | Get-SpectreEscapedText) -split "`r?`n"
                                    if ($currentHelpContentLines.Count -eq 0) {$currentHelpContentLines = @("[grey](No content for this help type)[/]")}
                                } catch {
                                    $currentHelpContentLines = @(("[red]Could not retrieve help: $($_.Exception.Message | Get-SpectreEscapedText)[/]" -split "`r?`n"))
                                }
                            }
                        }
                        ([System.ConsoleKey]::LeftArrow) {
                            $rightPaneView = 'Description'
                            $currentCommandObjectForHelp = $null
                            $currentHelpContentLines = @(); $helpContentScrollOffset = 0
                        }
                        ([System.ConsoleKey]::Enter) { # Mirror Right Arrow
                            $selectedHelpType = $helpOptions[$currentHelpOptionIndex]
                            $currentHelpContent = "[grey]Fetching help...[/]"
                            $currentHelpContentLines = @()
                            $helpContentScrollOffset = 0
                            $rightPaneView = 'HelpContent'
                            $LiveContext.Refresh()

                            if ($selectedHelpType -eq "Online") {
                                $currentHelpContent = "[yellow]Press Right Arrow or Enter to open online help (if available), or Left Arrow to go back.[/]"
                                $currentHelpContentLines = @($currentHelpContent.ToString())
                            } else {
                                try {
                                    $helpText = ""
                                    switch($selectedHelpType) {
                                        # "Synopsis" { $helpText = Get-Help $currentCommandObjectForHelp.CommandInfo | Out-String }
                                        "Examples" { $helpText = Get-Help $currentCommandObjectForHelp.CommandInfo -Examples | Out-String }
                                        "Detailed" { $helpText = Get-Help $currentCommandObjectForHelp.CommandInfo -Detailed | Out-String }
                                        "Full"     { $helpText = Get-Help $currentCommandObjectForHelp.CommandInfo -Full | Out-String }
                                    }
                                    $currentHelpContentLines = ($helpText | Get-SpectreEscapedText) -split "`r?`n"
                                    if ($currentHelpContentLines.Count -eq 0) {$currentHelpContentLines = @("[grey](No content for this help type)[/]")}
                                } catch {
                                    $currentHelpContentLines = @(("[red]Could not retrieve help: $($_.Exception.Message | Get-SpectreEscapedText)[/]" -split "`r?`n"))
                                }
                            }
                        }
                    }
                } elseif ($rightPaneView -eq 'HelpContent') {
                    switch ($keyInfo.Key) {
                        ([System.ConsoleKey]::LeftArrow) {
                            $rightPaneView = 'HelpOptions'
                            $currentHelpContentLines = @(); $helpContentScrollOffset = 0
                        }
                        ([System.ConsoleKey]::RightArrow) {
                            if ($helpOptions[$currentHelpOptionIndex] -eq "Online") {
                                try { Get-Help $currentCommandObjectForHelp.CommandInfo -Online
                                } catch {
                                    $currentHelpContentLines = "[red]Could not retrieve online help[/]"
                                }
                                $rightPaneView = 'HelpOptions'
                                $currentHelpContentLines = @(); $helpContentScrollOffset = 0
                            }
                        }
                        ([System.ConsoleKey]::Enter) {
                            if ($helpOptions[$currentHelpOptionIndex] -eq "Online") {
                                try { Get-Help $currentCommandObjectForHelp.CommandInfo -Online
                                }
                                catch {
                                    $currentHelpContentLines = "[red]Could not retrieve online help[/]"
                                }
                                $rightPaneView = 'HelpOptions'
                                $currentHelpContentLines = @(); $helpContentScrollOffset = 0
                            }
                        }
                        ([System.ConsoleKey]::UpArrow) {
                            if ($helpContentScrollOffset -gt 0) { $helpContentScrollOffset-- }
                        }
                        ([System.ConsoleKey]::DownArrow) {
                            if (($helpContentScrollOffset + $helpContentPageSize) -lt $currentHelpContentLines.Count) {
                                $helpContentScrollOffset++
                            }
                        }
                    }
                }
            } # End while
        }
        catch {
            Write-SpectreHost "[bold red]Error within Invoke-SpectreLive: $($_.Exception.ToString() | Get-SpectreEscapedText)[/]"
            Read-SpectrePause -Message "[grey]Press Enter to acknowledge error and return...[/]" -NoNewline
            return $null
        }
        return $null
    } # End Spectre Live session

    Clear-Host
}

<#
.SYNOPSIS
    Interactively explores available PowerShell modules and their commands.
 
.DESCRIPTION
    The Show-ModuleExplorer cmdlet provides an interactive, terminal-based user interface
    to browse through PowerShell modules installed or available on the system.
    Users can select a module from the list to view its commands using the Show-ModuleCommandViewer function.
 
    The interface displays "Module Explorer" as a title and lists all available modules.
    You can filter the list of modules by providing a search string to the -Filter parameter.
    The list also includes options to "Refresh List" and "<-- Exit" the explorer.
 
    This function utilizes PwshSpectreConsole cmdlets for a rich interactive experience.
 
.PARAMETER Filter
    An optional string used to filter the list of displayed modules.
    The function will search for modules whose names matches the filter string.
 
    Type: String
    Position: Named
    Default value: None
    Accept pipeline input: False
    Accept wildcard characters: True
 
.EXAMPLE
    PS C:\> Show-ModuleExplorer
 
    Description:
    Launches the Module Explorer, displaying all available PowerShell modules.
    You can then navigate and select a module to view its commands.
 
.EXAMPLE
    PS C:\> Show-ModuleExplorer -Filter "BurntToast"
 
    Description:
    Launches the Module Explorer and filters the initial list to show only modules
    named "BurnToast".
 
.NOTES
    This function depends on several cmdlets from a PowerShell module providing Spectre.Console integration
    (e.g., Write-SpectreFigletText, Read-SpectreSelection, Write-SpectreHost, Write-SpectreRule, Read-SpectrePause, Get-SpectreEscapedText)
    for its user interface. Ensure this module and its dependencies are installed and available.
 
    Upon selecting a module, this function calls `Show-ModuleCommandViewer` to display
    the commands within that module.
 
    The explorer allows for refreshing the module list to reflect any changes (installs/uninstalls)
    made while the explorer is running.
 
    Navigation within the selection list is done using arrow keys and Enter.
    The selection prompt also supports typing to filter the choices in real-time.
 
.INPUTS
    None
    This function does not accept input from the pipeline.
 
.OUTPUTS
    None
    This function does not return any objects to the pipeline. It provides an interactive display in the console.
 
.LINK
    None
#>

function Show-ModuleExplorer {
    [CmdletBinding()]
    param(
        [string]$Filter # Optional filter for module names
    )

    try {
        $moduleLookup = @{} # Initialize hashtable to map display names to module objects

        while ($true) {
            Clear-Host
            Write-SpectreFigletText -Text "Module Explorer" -Alignment "Center"
            $moduleQuery = @{ ListAvailable = $true }
            if ($Filter) {
                $moduleQuery.Name = $Filter
            }
            $availableModules = Get-Module @moduleQuery | Select-Object Name, Version, Path, ModuleBase, RootModule | Sort-Object Name

            if (-not $availableModules) {
                Write-SpectreHost "[bold red]No PowerShell modules found.[/]"
                Read-SpectrePause -Message "[grey]Press Enter to continue...[/]" -NoNewline
                return
            }

            $exitChoiceString = "[cyan]<-- Exit[/]"
            $refreshChoiceString = "[cyan]Refresh List[/]"
            # Reset the main loop if modules changes (install/remove)
            $moduleLookup.Clear()
            $moduleChoices = @($exitChoiceString, $refreshChoiceString)
            
            $moduleChoices += $availableModules | ForEach-Object {
                $versionString = if ($_.Version) { "v$($_.Version)" } else { "Version N/A" }
                $displayName = "$($_.Name) ($versionString)"
                $moduleLookup[$displayName] = $_ # Populate the lookup table
                $displayName
            }
            
            $promptTitle = "[yellow bold]Select a PowerShell Module to Explore (or Exit):[/]"
            Write-SpectreRule -Title "[grey] Installed Modules: $($availableModules.Count) [/]" -Alignment Center
            $selectedModuleDisplay = Read-SpectreSelection -Message $promptTitle -PageSize 15 -Choices $moduleChoices -EnableSearch

            if (-not $selectedModuleDisplay -or $selectedModuleDisplay -eq $exitChoiceString) {
                Write-SpectreHost "[yellow]Exiting Module Explorer.[/]"
                break
            }

            if ($selectedModuleDisplay -eq $refreshChoiceString) {
                Write-SpectreHost "[italic green]Refreshing module list...[/]"
                continue
            }

            # Use the lookup table
            $selectedModuleObject = $moduleLookup[$selectedModuleDisplay]

            if (-not $selectedModuleObject) {
                # This condition should not be met if $selectedModuleDisplay is from $moduleChoices
                Write-SpectreHost "[bold red]Error: Could not retrieve details for selected module: '$($selectedModuleDisplay | Get-SpectreEscapedText)'. This is unexpected.[/]"
                Read-SpectrePause -Message "[grey]Press Enter to continue...[/]" -NoNewline
                continue
            }
            
            Clear-Host
            Show-ModuleCommandViewer -SelectedModule $selectedModuleObject

        } # End of main loop
    }
    catch {
        Write-SpectreHost "[bold red]An unexpected error occurred in Module Explorer: $($_.Exception.ToString() | Get-SpectreEscapedText)[/]"
        Read-SpectrePause -Message "[grey]Press Enter to acknowledge error and exit...[/]" -NoNewline
    }
    finally {
        Clear-Host
        Write-SpectreHost "[cyan]Module Explorer session ended.[/]"
    }
}