Directory.Packages.props
+1
-0
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 98eabd0..f57233a 100644
@@ -14,5 +14,6 @@
<PackageVersion Include="Avalonia.Themes.Fluent" Version="12.0.0-rc2" />
<PackageVersion Include="PowerShellStandard.Library" Version="5.1.1" />
<PackageVersion Include="System.Management.Automation" Version="7.6.0" />
<PackageVersion Include="System.Text.Json" Version="10.0.5" />
</ItemGroup>
</Project>
OutGridTree.Window/MainWindow.axaml
+3
-2
diff --git a/OutGridTree.Window/MainWindow.axaml b/OutGridTree.Window/MainWindow.axaml
index 9b3204c..00a1097 100644
@@ -2,6 +2,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:w="clr-namespace:OutGridTree.Window"
xmlns:c="clr-namespace:OutGridTree.Window.Converters"
xmlns:ps="clr-namespace:System.Management.Automation;assembly=System.Management.Automation"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
@@ -16,8 +17,8 @@
SelectionMode="Extended" RowDetailsVisibilityMode="VisibleWhenSelected"
CanUserSortColumns="True" CanUserReorderColumns="True" CanUserResizeColumns="True">
<DataGrid.RowDetailsTemplate>
<DataTemplate x:DataType="{x:Type ps:PSObject}">
<TreeView ItemsSource="{Binding Properties}">
<DataTemplate x:DataType="{x:Type w:Row}">
<TreeView ItemsSource="{Binding Entry.Properties}">
<TreeView.ItemTemplate>
<TreeDataTemplate x:DataType="{x:Type ps:PSPropertyInfo}" ItemsSource="{Binding Value, Converter={StaticResource psObjectPropertiesConverter}}">
<Grid ColumnDefinitions="Auto,Auto" ColumnSpacing="10">
OutGridTree.Window/MainWindow.axaml.cs
+14
-73
diff --git a/OutGridTree.Window/MainWindow.axaml.cs b/OutGridTree.Window/MainWindow.axaml.cs
index 93a5eef..ae80449 100644
@@ -2,7 +2,6 @@ using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Management.Automation;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Data.Converters;
@@ -12,7 +11,7 @@ namespace OutGridTree.Window;
internal sealed partial class MainWindow : Avalonia.Controls.Window
{
private readonly RpcService rpcService;
private readonly ObservableCollection<object?> records = [];
private readonly ObservableCollection<Row?> records = [];
public MainWindow(RpcService rpcService)
{
@@ -33,97 +32,39 @@ internal sealed partial class MainWindow : Avalonia.Controls.Window
private void OnHeadersReceived(IEnumerable<string> headers)
{
Dispatcher.Post(() => SetPsObjectHeaders(headers));
Dispatcher.Post(() => SetHeaders(headers));
}
private void OnRecordReceived(object? record)
private void SetHeaders(IEnumerable<string> headers)
{
Dispatcher.Post(() => AddRecord(record));
}
private void AddRecord(object? record)
{
if (record is not null && Table.Columns.Count is 0)
{
if (record is PSObject psObject)
{
SetPsObjectHeaders(psObject.Properties.Select(p => p.Name));
}
else
{
SetPrimitiveHeader(record.GetType());
}
}
records.Add(record);
}
private void SetPsObjectHeaders(IEnumerable<string> headers)
{
foreach (var header in headers)
foreach (var (i, header) in headers.Select((h, i) => (i, h)))
{
Table.Columns.Add(
new DataGridTextColumn()
{
Header = header,
Binding = CompiledBinding.Create<object, object>(
o => o,
converter: new FuncValueConverter<object, string?>(o =>
(o as PSObject)?.Properties[header].Value?.ToString()
)
Binding = CompiledBinding.Create<Row?, Row?>(
r => r,
converter: new FuncValueConverter<Row?, string?>(r => r?.Headers.ElementAtOrDefault(i))
),
}
);
}
}
private void SetPrimitiveHeader(Type type)
private void OnRecordReceived(Row? record)
{
var header = "Primitive";
if (type == typeof(bool))
{
header = "Boolean";
}
else if (
type == typeof(byte)
|| type == typeof(sbyte)
|| type == typeof(short)
|| type == typeof(ushort)
|| type == typeof(int)
|| type == typeof(uint)
|| type == typeof(long)
|| type == typeof(ulong)
)
{
header = "Integer";
}
else if (type == typeof(float) || type == typeof(double) || type == typeof(decimal))
{
header = "Number";
}
else if (type == typeof(char))
{
header = "Character";
}
else if (type == typeof(string))
{
header = "String";
}
Dispatcher.Post(() => AddRecord(record));
}
Table.Columns.Add(
new DataGridTextColumn()
{
Header = header,
Binding = CompiledBinding.Create<object, object>(
o => o,
converter: new FuncValueConverter<object, string?>(o => o?.ToString())
),
}
);
private void AddRecord(Row? record)
{
records.Add(record);
}
protected override void OnClosed(EventArgs e)
{
rpcService.TitleReceived -= OnTitleReceived;
rpcService.HeadersReceived -= OnHeadersReceived;
rpcService.RecordReceived -= OnRecordReceived;
OutGridTree.Window/Row.cs
+5
-0
diff --git a/OutGridTree.Window/Row.cs b/OutGridTree.Window/Row.cs
new file mode 100644
index 0000000..40cc44a
@@ -0,0 +1,5 @@
using System.Management.Automation;
namespace OutGridTree.Window;
public sealed record Row(string[] Headers, PSObject? Entry);
OutGridTree.Window/RpcService.cs
+14
-5
diff --git a/OutGridTree.Window/RpcService.cs b/OutGridTree.Window/RpcService.cs
index 757d58d..1661dd0 100644
@@ -3,7 +3,7 @@ using System.Collections.Generic;
using System.IO;
using System.IO.Pipes;
using System.Management.Automation;
using System.Text;
using System.Text.Json;
using System.Threading;
namespace OutGridTree.Window;
@@ -12,7 +12,7 @@ internal sealed class RpcService
{
public event Action<string>? TitleReceived;
public event Action<IEnumerable<string>>? HeadersReceived;
public event Action<object?>? RecordReceived;
public event Action<Row?>? RecordReceived;
private NamedPipeClientStream? pipe;
private readonly CancellationTokenSource cts = new();
@@ -46,12 +46,21 @@ internal sealed class RpcService
}
else if (header is "HEADERS")
{
HeadersReceived?.Invoke(body.Split(','));
var headers = JsonSerializer.Deserialize<string[]>(body);
if (headers is null)
{
continue;
}
HeadersReceived?.Invoke(headers);
}
else if (header is "RECORD")
{
var record = PSSerializer.Deserialize(Encoding.UTF8.GetString(Convert.FromBase64String(body)));
RecordReceived?.Invoke(record);
var elements = JsonSerializer.Deserialize<string[]>(body);
if (elements is null)
{
continue;
}
RecordReceived?.Invoke(new(elements[..^1], PSSerializer.Deserialize(elements[^1]) as PSObject));
}
}
}
OutGridTree/ColumnFormatter.cs
+30
-0
diff --git a/OutGridTree/ColumnFormatter.cs b/OutGridTree/ColumnFormatter.cs
new file mode 100644
index 0000000..80558e1
@@ -0,0 +1,30 @@
using System;
using System.Management.Automation;
namespace OutGridTree;
internal abstract class ColumnFormatter
{
private ColumnFormatter() { }
public static ColumnFormatter FromDisplayEntry(DisplayEntry displayEntry) =>
displayEntry.ValueType switch
{
DisplayEntryValueType.Property => new Property(displayEntry.Value),
DisplayEntryValueType.ScriptBlock => new Script(ScriptBlock.Create(displayEntry.Value)),
_ => throw new ArgumentException(
$"Unrecognized {nameof(DisplayEntryValueType)}: {displayEntry.ValueType}.",
nameof(displayEntry)
),
};
public sealed class Property(string name) : ColumnFormatter
{
public string Name { get; } = name;
}
public sealed class Script(ScriptBlock scriptBlock) : ColumnFormatter
{
public ScriptBlock ScriptBlock { get; } = scriptBlock;
}
}
OutGridTree/OutGridTree.cs
+85
-3
diff --git a/OutGridTree/OutGridTree.cs b/OutGridTree/OutGridTree.cs
index 3b4ac67..a260c9a 100644
@@ -1,10 +1,12 @@
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;
using System.Text.Json;
namespace OutGridTree;
@@ -23,6 +25,8 @@ public sealed class OutGridTree : PSCmdlet
[Parameter]
public string[]? Headers { get; set; }
private bool hasSentHeaders = false;
private ColumnFormatter[]? columnFormats;
#nullable disable
private NamedPipeServerStream pipe;
@@ -61,7 +65,8 @@ public sealed class OutGridTree : PSCmdlet
if (MyInvocation.BoundParameters.ContainsKey(nameof(Headers)) && Headers is not null)
{
writer.WriteLine($"HEADERS: {string.Join(",", Headers)}");
writer.WriteLine($"HEADERS: {JsonSerializer.Serialize(Headers)}");
hasSentHeaders = true;
}
}
@@ -74,8 +79,14 @@ public sealed class OutGridTree : PSCmdlet
}
try
{
if (!hasSentHeaders)
{
SendFormatHeaders();
}
var headerValues = GetHeaderValues();
writer.WriteLine(
$"RECORD: {Convert.ToBase64String(Encoding.UTF8.GetBytes(PSSerializer.Serialize(InputObject)))}"
$"RECORD: {JsonSerializer.Serialize(headerValues.Append(PSSerializer.Serialize(InputObject)))}"
);
}
catch (IOException)
@@ -85,6 +96,77 @@ public sealed class OutGridTree : PSCmdlet
}
}
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)
OutGridTree/OutGridTree.csproj
+2
-1
diff --git a/OutGridTree/OutGridTree.csproj b/OutGridTree/OutGridTree.csproj
index 548d542..78f2481 100644
@@ -1,5 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<PackageReference Include="PowerShellStandard.Library" />
<PackageReference Include="PowerShellStandard.Library" PrivateAssets="all" />
<PackageReference Include="System.Text.Json" PrivateAssets="all" />
</ItemGroup>
</Project>
OutGridTree/packages.lock.json
+76
-0
diff --git a/OutGridTree/packages.lock.json b/OutGridTree/packages.lock.json
index 6765188..092c48a 100644
@@ -23,10 +23,86 @@
"resolved": "5.1.1",
"contentHash": "e31xJjG+Kjbv6YF3Yq6D4Dl3or8v7LrNF41k3CXrWozW6hR1zcOe5KYuZJaGSiAgLnwP8wcW+I3+IWEzMPZKXQ=="
},
"System.Text.Json": {
"type": "Direct",
"requested": "[10.0.5, )",
"resolved": "10.0.5",
"contentHash": "vW2zhkWziyfhoSXNf42mTWyilw+vfwBGOsODDsHSFtOIY6LCgfRVUyaAilLEL4Kc1fzhaxcep5pS0VWYPSDW0w==",
"dependencies": {
"Microsoft.Bcl.AsyncInterfaces": "10.0.5",
"System.Buffers": "4.6.1",
"System.IO.Pipelines": "10.0.5",
"System.Memory": "4.6.3",
"System.Runtime.CompilerServices.Unsafe": "6.1.2",
"System.Text.Encodings.Web": "10.0.5",
"System.Threading.Tasks.Extensions": "4.6.3"
}
},
"Microsoft.Bcl.AsyncInterfaces": {
"type": "Transitive",
"resolved": "10.0.5",
"contentHash": "hQB3Hq1LlF0NkGVNyZIvwIQIY3LM7Cw1oYjNiTvdNqmzzipVAWEK1c5sj2H5aFX0udnjgPLxSYKq2fupueS8ow==",
"dependencies": {
"System.Threading.Tasks.Extensions": "4.6.3"
}
},
"Microsoft.NETCore.Platforms": {
"type": "Transitive",
"resolved": "1.1.0",
"contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A=="
},
"System.Buffers": {
"type": "Transitive",
"resolved": "4.6.1",
"contentHash": "N8GXpmiLMtljq7gwvyS+1QvKT/W2J8sNAvx+HVg4NGmsG/H+2k/y9QI23auLJRterrzCiDH+IWAw4V/GPwsMlw=="
},
"System.IO.Pipelines": {
"type": "Transitive",
"resolved": "10.0.5",
"contentHash": "8/ZHN/j2y1t+7McdCf1wXku2/c7wtrGLz3WQabIoPuLAn3bHDWT6YOJYreJq8sCMPSo6c8iVYXUdLlFGX5PEqw==",
"dependencies": {
"System.Buffers": "4.6.1",
"System.Memory": "4.6.3",
"System.Threading.Tasks.Extensions": "4.6.3"
}
},
"System.Memory": {
"type": "Transitive",
"resolved": "4.6.3",
"contentHash": "qdcDOgnFZY40+Q9876JUHnlHu7bosOHX8XISRoH94fwk6hgaeQGSgfZd8srWRZNt5bV9ZW2TljcegDNxsf+96A==",
"dependencies": {
"System.Buffers": "4.6.1",
"System.Numerics.Vectors": "4.6.1",
"System.Runtime.CompilerServices.Unsafe": "6.1.2"
}
},
"System.Numerics.Vectors": {
"type": "Transitive",
"resolved": "4.6.1",
"contentHash": "sQxefTnhagrhoq2ReR0D/6K0zJcr9Hrd6kikeXsA1I8kOCboTavcUC4r7TSfpKFeE163uMuxZcyfO1mGO3EN8Q=="
},
"System.Runtime.CompilerServices.Unsafe": {
"type": "Transitive",
"resolved": "6.1.2",
"contentHash": "2hBr6zdbIBTDE3EhK7NSVNdX58uTK6iHW/P/Axmm9sl1xoGSLqDvMtpecn226TNwHByFokYwJmt/aQQNlO5CRw=="
},
"System.Text.Encodings.Web": {
"type": "Transitive",
"resolved": "10.0.5",
"contentHash": "opvD/nKTzGKA7GVntZ9L823kN6IxgHQfuxY+VI9gv8VE1Y7CSKoi/QS1EYDQiA63MqtZsD7X6zkISd2ZQJohTQ==",
"dependencies": {
"System.Buffers": "4.6.1",
"System.Memory": "4.6.3",
"System.Runtime.CompilerServices.Unsafe": "6.1.2"
}
},
"System.Threading.Tasks.Extensions": {
"type": "Transitive",
"resolved": "4.6.3",
"contentHash": "7sCiwilJLYbTZELaKnc7RecBBXWXA+xMLQWZKWawBxYjp6DBlSE3v9/UcvKBvr1vv2tTOhipiogM8rRmxlhrVA==",
"dependencies": {
"System.Runtime.CompilerServices.Unsafe": "6.1.2"
}
}
},
".NETStandard,Version=v2.0/linux-arm64": {},