public/Out-GridForm.ps1

function Out-GridForm {
    <#
    .SYNOPSIS
        Displays a grid form for selecting objects from the pipeline.

    .DESCRIPTION
        Out-GridForm presents the input objects in a sortable, selectable grid using a PyQt5 dialog.

    .PARAMETER InputObject
        The objects to display in the grid.

    .PARAMETER Title
        The title of the grid form window.

    .PARAMETER OutputMode
        Specifies whether a single or multiple rows can be selected. Valid values are 'Single' and 'Multiple'.

    .PARAMETER NumSortColumn
        An array of column names that should be sorted numerically.
        If omitted, columns are sorted lexicographically, example "9" > "10".

    .EXAMPLE
        Get-Process | Out-GridForm -Title "Select Processes" -OutputMode Multiple

        Displays a grid form with the processes, allowing multiple selection.
    
    .EXAMPLE
        Get-Process | Out-GridForm -Title "Select Process" -OutputMode Single

        Displays a grid form with the processes, allowing a single selection.
    
    .EXAMPLE
        Get-Process | Out-GridForm -Title "Select Processes" -OutputMode Multiple -NumSortColumn "CPU"

        Displays a grid form with the processes, allowing multiple selection, and sorts the "CPU" column numerically.
    .LINK
        https://github.com/Skatterbrainz/linuxtools/blob/master/docs/Out-GridForm.md
    .NOTES
        This function requires PyQt5 to be installed.
        Ensure that you have the necessary permissions to run this command.
        Use with caution, as it will display and potentially allow selection of sensitive data.
    #>

    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline=$true, Mandatory=$true)]
        [PSObject[]]$InputObject,
        [string]$Title = "Grid Form",
        [ValidateSet('Single','Multiple')]
        [string]$OutputMode = 'Single',
        [string[]]$NumSortColumn = @()
    )

    begin { $objects = @() }
    process { $objects += $InputObject }
    end {
        if ($objects.Count -eq 0) { return }

        $properties = $objects[0].PSObject.Properties.Name

        $rows = $objects | ForEach-Object {
            $obj = [PSCustomObject]@{}
            foreach ($p in $properties) {
                $obj | Add-Member -NotePropertyName $p -NotePropertyValue ([string]$($_.$p))
            }
            $obj
        }

        $json = ([PSCustomObject]@{
            columns      = $properties
            title        = $Title
            rows         = $rows
            outputMode   = $OutputMode
            numSortCols  = $NumSortColumn
        }) | ConvertTo-Json -Depth 5

        $pyPath   = [System.IO.Path]::GetTempFileName()
        $dataPath = [System.IO.Path]::GetTempFileName()

        $pyScript = @'
import sys, json
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (QApplication, QTableWidget, QTableWidgetItem,
                             QDialog, QDialogButtonBox, QVBoxLayout,
                             QHeaderView)

with open(sys.argv[1], 'r') as f:
    data = json.load(f)

cols = data['columns']
rows = data['rows']
mode = data.get('outputMode', 'Single')
num_cols = set(data.get('numSortCols', []))

app = QApplication(sys.argv)
dlg = QDialog()
dlg.setWindowTitle(data.get('title', 'Grid'))
dlg.resize(900, 500)

table = QTableWidget(len(rows), len(cols))
table.setHorizontalHeaderLabels(cols)
table.setEditTriggers(QTableWidget.NoEditTriggers)
table.setSelectionBehavior(QTableWidget.SelectRows)

if mode == 'Multiple':
    table.setSelectionMode(QTableWidget.ExtendedSelection)
else:
    table.setSelectionMode(QTableWidget.SingleSelection)

# Populate data
for r, row in enumerate(rows):
    for c, col in enumerate(cols):
        text = str(row.get(col, ''))
        item = QTableWidgetItem(text)
        table.setItem(r, c, item)
        # Set numeric sort key for designated columns
        if col in num_cols:
            try:
                item.setData(Qt.UserRole, float(text))
            except ValueError:
                pass # non-numeric value; will sort last

# Enable sorting AFTER data is in place
table.setSortingEnabled(True)
table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)

buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
buttons.accepted.connect(dlg.accept)
buttons.rejected.connect(dlg.reject)

layout = QVBoxLayout()
layout.addWidget(table)
layout.addWidget(buttons)
dlg.setLayout(layout)

if dlg.exec_() == QDialog.Accepted:
    selected = sorted(set(table.selectionModel().selectedRows()))
    result = []
    for idx in selected:
        row_data = {}
        for c, col in enumerate(cols):
            item = table.item(idx.row(), c)
            row_data[col] = item.text() if item else ''
        result.append(row_data)
    sys.stdout.write(json.dumps(result))

sys.exit(0)
'@


    try {
        Set-Content -Path $pyPath   -Value $pyScript -Encoding UTF8
        Set-Content -Path $dataPath -Value $json     -Encoding UTF8

        $output = & python3 $pyPath $dataPath 2>$null

        if ($LASTEXITCODE -eq 0 -and $output) {
            $selected = $output | ConvertFrom-Json
            if ($OutputMode -eq 'Single') {
                $selected | Select-Object -First 1
            } else {
                $selected
            }
        }
    }
    finally {
        Remove-Item $pyPath, $dataPath -Force -ErrorAction SilentlyContinue
    }
}}