📄 OutGridTree/OutGridTree.cs
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Pipes;
using System.Linq;
using System.Management.Automation;
using System.Management.Automation.Internal;
using System.Text.Json;

namespace OutGridTree;

[Cmdlet(VerbsData.Out, "GridTree")]
[Alias("ogt")]
public sealed class OutGridTree : PSCmdlet
{
    [Parameter(ValueFromPipeline = true)]
    public PSObject InputObject { get; set; } = AutomationNull.Value;

    [Parameter(Mandatory = true)]
    public string WindowExe { get; set; } = "";

    [Parameter]
    public string? Title { get; set; }

    [Parameter]
    public string[]? Headers { get; set; }
    private bool hasSentHeaders = false;
    private ColumnFormatter[]? columnFormats;

#nullable disable
    private NamedPipeServerStream pipe;
    private Process windowProcess;
    private StreamWriter writer;

#nullable restore

    protected override void BeginProcessing()
    {
        var pipeName = Path.GetTempFileName();
        pipe = new(pipeName, PipeDirection.Out);

        windowProcess = Process.Start(
            new ProcessStartInfo()
            {
                FileName = WindowExe,
                Arguments = pipeName,
                UseShellExecute = false,
                RedirectStandardOutput = true,
                RedirectStandardError = true,
            }
        );
        pipe.WaitForConnection();

        writer = new(pipe);

        if (MyInvocation.BoundParameters.ContainsKey(nameof(Title)) && Title is not null)
        {
            writer.WriteLine($"TITLE: {Title}");
        }
        else
        {
            writer.WriteLine($"TITLE: {MyInvocation.Line}");
        }

        if (MyInvocation.BoundParameters.ContainsKey(nameof(Headers)) && Headers is not null)
        {
            writer.WriteLine($"HEADERS: {JsonSerializer.Serialize(Headers)}");
            hasSentHeaders = true;
        }
    }

    protected override void ProcessRecord()
    {
        if (!pipe.IsConnected)
        {
            TerminateDueToWindowClosed();
            return;
        }
        try
        {
            if (!hasSentHeaders)
            {
                SendFormatHeaders();
            }

            var headerValues = GetHeaderValues();
            writer.WriteLine(
                $"RECORD: {JsonSerializer.Serialize(headerValues.Append(PSSerializer.Serialize(InputObject)))}"
            );
        }
        catch (IOException)
        {
            TerminateDueToWindowClosed();
            return;
        }
    }

    private void SendFormatHeaders()
    {
        string[] headers;

        var formatResult = InvokeCommand.InvokeScript("Get-FormatData $args[0]", InputObject.BaseObject.GetType());
        if (
            formatResult.Count is 1
            && formatResult[0].BaseObject is ExtendedTypeDefinition { FormatViewDefinition: var views }
            && views.FirstOrDefault(v => v.Control is TableControl) is { Control: TableControl tableControl }
            && tableControl.Rows.FirstOrDefault(r => r.Columns.Count == tableControl.Headers.Count)
                is { Columns: var columns }
        )
        {
            headers = [.. tableControl.Headers.Zip(columns, (h, c) => h.Label ?? c.DisplayEntry.Value)];
            columnFormats = [.. columns.Select(c => ColumnFormatter.FromDisplayEntry(c.DisplayEntry))];
        }
        else if (InputObject is PSObject psObject && !psObject.BaseObject.GetType().IsPrimitive)
        {
            Headers = headers = [.. psObject.Properties.Select(p => p.Name)];
        }
        else
        {
            headers =
            [
                InputObject.BaseObject switch
                {
                    bool => "Boolean",
                    byte or sbyte or short or ushort or int or uint or long or ulong => "Integer",
                    float or double or decimal => "Number",
                    char => "Character",
                    string => "String",
                    _ => "Primitive",
                },
            ];
        }

        writer.WriteLine($"HEADERS: {JsonSerializer.Serialize(headers)}");
        hasSentHeaders = true;
    }

    private IEnumerable<string?> GetHeaderValues()
    {
        if (columnFormats is not null)
        {
            return columnFormats.Select(f =>
                f switch
                {
                    ColumnFormatter.Property prop => InputObject
                        .Properties.Match(prop.Name)
                        .FirstOrDefault()
                        ?.Value.ToString(),
                    ColumnFormatter.Script script => script
                        .ScriptBlock.InvokeWithContext([], [new("_", InputObject)])
                        .FirstOrDefault()
                        is { } value
                        ? value.ToString()
                        : null,
                    _ => null,
                }
            );
        }
        else if (InputObject is PSObject psObject && !psObject.BaseObject.GetType().IsPrimitive)
        {
            return Headers.Select(h => InputObject.Properties.Match(h).FirstOrDefault()?.Value?.ToString());
        }
        else
        {
            return [InputObject?.ToString()];
        }
    }

    private void TerminateDueToWindowClosed() =>
        ThrowTerminatingError(
            new(new InvalidOperationException("Window closed"), "WindowClosed", ErrorCategory.InvalidOperation, null)
        );

    protected override void EndProcessing()
    {
        // TODO: Wait for window to close
        if (pipe.IsConnected)
        {
            writer.Flush();
        }
    }

    protected override void StopProcessing()
    {
        if (pipe.IsConnected)
        {
            pipe?.Close();
            pipe?.Disconnect();
        }
        pipe?.Dispose();
        if (!windowProcess.HasExited)
        {
            windowProcess?.Close();
        }
        windowProcess?.Dispose();
    }
}