Private/Console/Write-SpectreTable.ps1

# PSGuerrilla - Jim Tyler, Microsoft MVP - CC BY 4.0
# https://github.com/jimrtyler/PSGuerrilla | https://creativecommons.org/licenses/by/4.0/
# AI/LLM use: see AI-USAGE.md for required attribution
function Write-SpectreTable {
    <#
    .SYNOPSIS
        Renders a themed table using Spectre.Console when available, falling back to box-drawing characters.
    .PARAMETER Title
        Optional table title.
    .PARAMETER Columns
        Array of column definition hashtables: @{ Name = 'Col'; Color = 'Olive'; Alignment = 'Left' }
    .PARAMETER Rows
        Array of row arrays. Each row is an array of cell values (strings).
    .PARAMETER RowColors
        Optional array of guerrilla color names, one per row. Colors the entire row.
    .PARAMETER BorderColor
        Guerrilla color name for the table border. Default: 'Dim'.
    .PARAMETER HideBorder
        Suppress the table border entirely.
    #>

    [CmdletBinding()]
    param(
        [string]$Title,

        [Parameter(Mandatory)]
        [hashtable[]]$Columns,

        [Parameter(Mandatory)]
        [array[]]$Rows,

        [string[]]$RowColors,
        [string]$BorderColor = 'Dim',
        [switch]$HideBorder
    )

    if ($script:HasSpectre) {
        Write-SpectreTableEnhanced @PSBoundParameters
    } else {
        Write-SpectreTableFallback @PSBoundParameters
    }
}

function Write-SpectreTableEnhanced {
    [CmdletBinding()]
    param(
        [string]$Title,
        [hashtable[]]$Columns,
        [array[]]$Rows,
        [string[]]$RowColors,
        [string]$BorderColor = 'Dim',
        [switch]$HideBorder
    )

    $table = [Spectre.Console.Table]::new()

    if ($HideBorder) {
        $table.Border = [Spectre.Console.TableBorder]::None
    } else {
        $table.Border = [Spectre.Console.TableBorder]::Rounded
        $table.BorderColor($script:SpectreColors[$BorderColor] ?? $script:SpectreColors.Dim)
    }

    if ($Title) {
        $titleMarkup = "[bold $($script:SpectreColors.Olive.ToMarkup())]$([Spectre.Console.Markup]::Escape($Title))[/]"
        $table.Title = [Spectre.Console.TableTitle]::new($titleMarkup)
    }

    foreach ($col in $Columns) {
        $colColor = $script:SpectreColors[$col.Color] ?? $script:SpectreColors.Olive
        $colMarkup = "[$($colColor.ToMarkup()) bold]$([Spectre.Console.Markup]::Escape($col.Name))[/]"
        $tableCol = [Spectre.Console.TableColumn]::new($colMarkup)

        if ($col.Alignment -eq 'Right') {
            $tableCol.Alignment = [Spectre.Console.Justify]::Right
        } elseif ($col.Alignment -eq 'Center') {
            $tableCol.Alignment = [Spectre.Console.Justify]::Center
        }

        $table.AddColumn($tableCol)
    }

    for ($i = 0; $i -lt $Rows.Count; $i++) {
        $row = $Rows[$i]
        $rowColor = if ($RowColors -and $i -lt $RowColors.Count -and $RowColors[$i]) {
            $script:SpectreColors[$RowColors[$i]] ?? $script:SpectreColors.Parchment
        } else {
            $script:SpectreColors.Parchment
        }

        $cells = @()
        for ($j = 0; $j -lt $row.Count; $j++) {
            $cellText = [Spectre.Console.Markup]::Escape([string]$row[$j])
            # Use column color for first column, row color for data
            if ($j -eq 0) {
                $colColor = $script:SpectreColors[$Columns[$j].Color] ?? $script:SpectreColors.Olive
                $cells += [Spectre.Console.Markup]::new("[$($colColor.ToMarkup())]$cellText[/]")
            } else {
                $cells += [Spectre.Console.Markup]::new("[$($rowColor.ToMarkup())]$cellText[/]")
            }
        }

        $table.AddRow($cells)
    }

    [Spectre.Console.AnsiConsole]::Write($table)
}

function Write-SpectreTableFallback {
    [CmdletBinding()]
    param(
        [string]$Title,
        [hashtable[]]$Columns,
        [array[]]$Rows,
        [string[]]$RowColors,
        [string]$BorderColor = 'Dim',
        [switch]$HideBorder
    )

    # Calculate column widths
    $widths = @()
    for ($j = 0; $j -lt $Columns.Count; $j++) {
        $maxWidth = $Columns[$j].Name.Length
        foreach ($row in $Rows) {
            if ($j -lt $row.Count) {
                $cellLen = ([string]$row[$j]).Length
                if ($cellLen -gt $maxWidth) { $maxWidth = $cellLen }
            }
        }
        $widths += [Math]::Min($maxWidth, 50)
    }

    if (-not $HideBorder) {
        # Top border
        $topLine = ' ' + [char]0x250C  # ┌
        for ($j = 0; $j -lt $widths.Count; $j++) {
            $topLine += [string]::new([char]0x2500, $widths[$j] + 2)  # ─
            $topLine += if ($j -lt $widths.Count - 1) { [char]0x252C } else { [char]0x2510 }  # ┬ or ┐
        }
        Write-GuerrillaText $topLine -Color $BorderColor
    }

    # Header row
    $headerLine = ' ' + $(if (-not $HideBorder) { [char]0x2502 + ' ' } else { ' ' })
    for ($j = 0; $j -lt $Columns.Count; $j++) {
        $padded = if ($Columns[$j].Alignment -eq 'Right') {
            $Columns[$j].Name.PadLeft($widths[$j])
        } else {
            $Columns[$j].Name.PadRight($widths[$j])
        }
        Write-GuerrillaText $headerLine -Color $BorderColor -NoNewline
        Write-GuerrillaText $padded -Color ($Columns[$j].Color ?? 'Olive') -NoNewline
        $headerLine = if (-not $HideBorder) { ' ' + [char]0x2502 + ' ' } else { ' ' }
    }
    if (-not $HideBorder) {
        Write-GuerrillaText " $([char]0x2502)" -Color $BorderColor
    } else {
        Write-Host ''
    }

    if (-not $HideBorder) {
        # Separator
        $sepLine = ' ' + [char]0x251C  # ├
        for ($j = 0; $j -lt $widths.Count; $j++) {
            $sepLine += [string]::new([char]0x2500, $widths[$j] + 2)  # ─
            $sepLine += if ($j -lt $widths.Count - 1) { [char]0x253C } else { [char]0x2524 }  # ┼ or ┤
        }
        Write-GuerrillaText $sepLine -Color $BorderColor
    }

    # Data rows
    for ($i = 0; $i -lt $Rows.Count; $i++) {
        $row = $Rows[$i]
        $rowColor = if ($RowColors -and $i -lt $RowColors.Count -and $RowColors[$i]) { $RowColors[$i] } else { 'Parchment' }

        $prefix = ' ' + $(if (-not $HideBorder) { [char]0x2502 + ' ' } else { ' ' })
        for ($j = 0; $j -lt $Columns.Count; $j++) {
            $cellText = if ($j -lt $row.Count) { [string]$row[$j] } else { '' }
            $padded = if ($Columns[$j].Alignment -eq 'Right') {
                $cellText.PadLeft($widths[$j])
            } else {
                $cellText.PadRight($widths[$j])
            }
            # Truncate if too long
            if ($padded.Length -gt $widths[$j]) {
                $padded = $padded.Substring(0, $widths[$j] - 3) + '...'
            }

            Write-GuerrillaText $prefix -Color $BorderColor -NoNewline
            $cellColor = if ($j -eq 0) { $Columns[$j].Color ?? 'Olive' } else { $rowColor }
            Write-GuerrillaText $padded -Color $cellColor -NoNewline
            $prefix = if (-not $HideBorder) { ' ' + [char]0x2502 + ' ' } else { ' ' }
        }
        if (-not $HideBorder) {
            Write-GuerrillaText " $([char]0x2502)" -Color $BorderColor
        } else {
            Write-Host ''
        }
    }

    if (-not $HideBorder) {
        # Bottom border
        $botLine = ' ' + [char]0x2514  # └
        for ($j = 0; $j -lt $widths.Count; $j++) {
            $botLine += [string]::new([char]0x2500, $widths[$j] + 2)  # ─
            $botLine += if ($j -lt $widths.Count - 1) { [char]0x2534 } else { [char]0x2518 }  # ┴ or ┘
        }
        Write-GuerrillaText $botLine -Color $BorderColor
    }
}