Private/Format-NetworkStatisticMonitorFrame.ps1

#Requires -Version 5.1

function Format-NetworkStatisticMonitorFrame {
    <#
        .SYNOPSIS
            Renders a single text frame for Show-NetworkStatisticMonitor (pure formatter, no I/O).
 
        .DESCRIPTION
            Accepts a pre-sorted snapshot of network connection objects and returns the
            complete console frame as a [string]. Contains no interactive or I/O calls,
            making it fully unit-testable without a live terminal or network stack.
 
        .PARAMETER SortedConnections
            Array of connection objects (already sorted) with properties:
            Protocol, LocalAddress, LocalPort, RemoteAddress, RemotePort, State, ProcessName.
 
        .PARAMETER ComputerList
            Display string of monitored computers (e.g. "SRV01, SRV02").
 
        .PARAMETER CurrentSortMode
            Active sort column. Valid values: Process, Protocol, State, LocalPort, RemoteAddr.
 
        .PARAMETER SortDescending
            When $true, the descending sort indicator is shown next to the sort column.
 
        .PARAMETER Paused
            When $true, the (PAUSED) indicator is shown in the header.
 
        .PARAMETER TimeStr
            Pre-formatted timestamp string to display in the header.
 
        .PARAMETER Width
            Terminal width in columns used for line padding.
 
        .PARAMETER Height
            Terminal height in rows used for available data rows and trailing erase.
 
        .PARAMETER RefreshInterval
            Refresh interval in seconds displayed in the footer.
 
        .PARAMETER NoColor
            When set, ANSI escape sequences are suppressed.
 
        .OUTPUTS
            System.String
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param (
        [Parameter(Mandatory = $false)]
        [object[]]$SortedConnections = @(),

        [Parameter(Mandatory = $true)]
        [string]$ComputerList,

        [Parameter(Mandatory = $true)]
        [ValidateSet('Process', 'Protocol', 'State', 'LocalPort', 'RemoteAddr')]
        [string]$CurrentSortMode,

        [Parameter(Mandatory = $false)]
        [bool]$SortDescending = $false,

        [Parameter(Mandatory = $false)]
        [bool]$Paused = $false,

        [Parameter(Mandatory = $true)]
        [string]$TimeStr,

        [Parameter(Mandatory = $false)]
        [int]$Width = 80,

        [Parameter(Mandatory = $false)]
        [int]$Height = 24,

        [Parameter(Mandatory = $false)]
        [int]$RefreshInterval = 2,

        [Parameter(Mandatory = $false)]
        [switch]$NoColor
    )

    $esc      = [char]27
    $useColor = -not $NoColor.IsPresent

    $dim       = if ($useColor) { "${esc}[90m" }  else { '' }
    $reset     = if ($useColor) { "${esc}[0m" }   else { '' }
    $bold      = if ($useColor) { "${esc}[1m" }   else { '' }
    $cyan      = if ($useColor) { "${esc}[96m" }  else { '' }
    $white     = if ($useColor) { "${esc}[97m" }  else { '' }
    $yellow    = if ($useColor) { "${esc}[93m" }  else { '' }
    $green     = if ($useColor) { "${esc}[92m" }  else { '' }
    $red       = if ($useColor) { "${esc}[91m" }  else { '' }
    $underline = if ($useColor) { "${esc}[4m" }   else { '' }

    function Get-VisualWidth {
        param([string]$Text)
        ($Text -replace "$([char]27)\[\d+(?:;\d+)*m", '').Length
    }

    function ConvertTo-PaddedLine {
        param([string]$Text, [int]$TargetWidth)
        $visual = Get-VisualWidth -Text $Text
        $needed = $TargetWidth - $visual
        if ($needed -gt 0) { return $Text + [string]::new(' ', $needed) }
        return $Text
    }

    function Get-ProtocolColor {
        param([string]$Proto)
        if (-not $useColor) { return '' }
        if ($Proto -eq 'TCP') { return $cyan }
        if ($Proto -eq 'UDP') { return $yellow }
        return ''
    }

    function Get-StateColor {
        param([string]$ConnState)
        if (-not $useColor) { return '' }
        switch ($ConnState) {
            'Established' { return $green }
            'Listen'      { return "${white}${bold}" }
            'TimeWait'    { return $dim }
            'CloseWait'   { return $red }
            'Closing'     { return $red }
            'LastAck'     { return $red }
            default       { return '' }
        }
    }

    $connectionCount = if ($null -eq $SortedConnections) { 0 } else { $SortedConnections.Count }

    $frame     = [System.Text.StringBuilder]::new(4096)
    $lineCount = 0
    $separator = [string]::new('-', [math]::Min($Width - 4, 120))

    # Header
    $pauseIndicator = if ($Paused) { " ${red}${bold}(PAUSED)${reset}" } else { '' }
    $headerLine = " ${bold}${cyan}Network Monitor${reset} ${dim}-${reset} ${bold}${white}${ComputerList}${reset}${pauseIndicator} ${dim}|${reset} ${dim}${TimeStr}${reset}"
    [void]$frame.AppendLine((ConvertTo-PaddedLine -Text $headerLine -TargetWidth $Width))
    $lineCount++
    [void]$frame.AppendLine((ConvertTo-PaddedLine -Text " ${dim}${separator}${reset}" -TargetWidth $Width))
    $lineCount++

    # Column widths
    $colProto = 7; $colLAddr = 23; $colLPort = 12
    $colRAddr = 23; $colRPort = 13; $colState = 14

    # Sort direction arrow
    $sortArrow = if ($SortDescending) {
        if ($useColor) { "${cyan}v${reset}" } else { 'v' }
    } else {
        if ($useColor) { "${cyan}^${reset}" } else { '^' }
    }

    # Table header with active sort highlighted
    $protoH = if ($CurrentSortMode -eq 'Protocol')   { "${cyan}${underline}PROTO${reset}" }          else { "${dim}PROTO${reset}" }
    $lAddrH = "${dim}LOCAL ADDRESS${reset}"
    $lPortH = if ($CurrentSortMode -eq 'LocalPort')  { "${cyan}${underline}LOCAL PORT${reset}" }     else { "${dim}LOCAL PORT${reset}" }
    $rAddrH = if ($CurrentSortMode -eq 'RemoteAddr') { "${cyan}${underline}REMOTE ADDRESS${reset}" } else { "${dim}REMOTE ADDRESS${reset}" }
    $rPortH = "${dim}REMOTE PORT${reset}"
    $stateH = if ($CurrentSortMode -eq 'State')      { "${cyan}${underline}STATE${reset}" }          else { "${dim}STATE${reset}" }
    $procH  = if ($CurrentSortMode -eq 'Process')    { "${cyan}${underline}PROCESS${reset}" }        else { "${dim}PROCESS${reset}" }

    $headerRow = ' {0}{1}{2}{3}{4}{5}{6}' -f `
        (ConvertTo-PaddedLine -Text $protoH -TargetWidth $colProto),
        (ConvertTo-PaddedLine -Text $lAddrH -TargetWidth $colLAddr),
        (ConvertTo-PaddedLine -Text $lPortH -TargetWidth $colLPort),
        (ConvertTo-PaddedLine -Text $rAddrH -TargetWidth $colRAddr),
        (ConvertTo-PaddedLine -Text $rPortH -TargetWidth $colRPort),
        (ConvertTo-PaddedLine -Text $stateH -TargetWidth $colState),
        $procH
    [void]$frame.AppendLine((ConvertTo-PaddedLine -Text $headerRow -TargetWidth $Width))
    $lineCount++

    $dashRow = " ${dim}{0}{1}{2}{3}{4}{5}{6}${reset}" -f `
        ('{0,-7}'  -f '-----'),
        ('{0,-23}' -f '-------------'),
        ('{0,-12}' -f '----------'),
        ('{0,-23}' -f '--------------'),
        ('{0,-13}' -f '-----------'),
        ('{0,-14}' -f '-----'),
        '-------'
    [void]$frame.AppendLine((ConvertTo-PaddedLine -Text $dashRow -TargetWidth $Width))
    $lineCount++

    # Data rows
    $availableRows = $Height - $lineCount - 4
    if ($connectionCount -gt 0) {
        $displayCount = [math]::Min($connectionCount, [math]::Max(5, $availableRows))
        for ($i = 0; $i -lt $displayCount; $i++) {
            $conn       = $SortedConnections[$i]
            $protoColor = Get-ProtocolColor -Proto $conn.Protocol
            $stateColor = Get-StateColor -ConnState $conn.State

            $dataRow = ' {0} {1} {2} {3} {4} {5} {6}' -f `
                "${protoColor}$('{0,-5}' -f $conn.Protocol)${reset}",
                ('{0,-21}' -f $conn.LocalAddress),
                ('{0,-10}' -f $conn.LocalPort),
                ('{0,-21}' -f $conn.RemoteAddress),
                ('{0,-11}' -f $conn.RemotePort),
                "${stateColor}$('{0,-12}' -f $conn.State)${reset}",
                "${white}$($conn.ProcessName)${reset}"

            [void]$frame.AppendLine((ConvertTo-PaddedLine -Text $dataRow -TargetWidth $Width))
            $lineCount++
        }
    }
    else {
        [void]$frame.AppendLine((ConvertTo-PaddedLine -Text " ${yellow}(No matching connections found)${reset}" -TargetWidth $Width))
        $lineCount++
    }

    # Footer
    [void]$frame.AppendLine('')
    $lineCount++
    [void]$frame.AppendLine((ConvertTo-PaddedLine -Text " ${dim}${separator}${reset}" -TargetWidth $Width))
    $lineCount++

    $footerLine = " ${bold}[${cyan}Q${reset}${bold}]${reset}uit ${bold}[${cyan}S${reset}${bold}]${reset}ort ${bold}[${cyan}R${reset}${bold}]${reset}everse ${bold}[${cyan}P${reset}${bold}]${reset}ause ${dim}|${reset} Refresh: ${yellow}${RefreshInterval}s${reset} ${dim}|${reset} Sort: ${cyan}${bold}${CurrentSortMode}${reset} ${sortArrow} ${dim}|${reset} Connections: ${white}${connectionCount}${reset}"
    [void]$frame.AppendLine((ConvertTo-PaddedLine -Text $footerLine -TargetWidth $Width))
    $lineCount++

    # Erase trailing lines
    $remainingRows = $Height - $lineCount
    for ($r = 0; $r -lt $remainingRows; $r++) {
        [void]$frame.AppendLine("${esc}[2K")
    }

    return $frame.ToString()
}