diff --git a/PwshSpectreConsole.Tests/TestHelpers.psm1 b/PwshSpectreConsole.Tests/TestHelpers.psm1 index fdb361d7..46a9c1f6 100644 --- a/PwshSpectreConsole.Tests/TestHelpers.psm1 +++ b/PwshSpectreConsole.Tests/TestHelpers.psm1 @@ -2,14 +2,14 @@ using namespace Spectre.Console function Get-RandomColor { $type = 1 # Get-Random -Minimum 0 -Maximum 2 - switch($type) { + switch ($type) { 0 { $colors = [Spectre.Console.Color] | Get-Member -Static -Type Properties | Select-Object -ExpandProperty Name return $colors[$(Get-Random -Minimum 0 -Maximum $colors.Count)] } 1 { $hex = @() - for($i = 0; $i -lt 3; $i++) { + for ($i = 0; $i -lt 3; $i++) { $value = Get-Random -Minimum 0 -Maximum 255 $hex += [byte]$value } @@ -28,7 +28,7 @@ function Get-RandomList { ) $items = @() $count = Get-Random -Minimum $MinItems -Maximum $MaxItems - for($i = 0; $i -lt $count; $i++) { + for ($i = 0; $i -lt $count; $i++) { $items += $Generator.Invoke() } return $items @@ -39,7 +39,7 @@ function Get-RandomString { $length = Get-Random -Minimum 10 -Maximum 20 $chars = [char[]]([char]'a'..[char]'z' + [char]'A'..[char]'Z' + [char]'0'..[char]'9') $string = "" - for($i = 0; $i -lt $length; $i++) { + for ($i = 0; $i -lt $length; $i++) { $string += $chars[$(Get-Random -Minimum 0 -Maximum $chars.Count)] } return $string @@ -83,23 +83,23 @@ function Get-RandomTree { [int] $CurrentDepth = 0 ) - if($CurrentDepth -gt $MaxDepth) { + if ($CurrentDepth -gt $MaxDepth) { return $Root } $CurrentDepth++ - if($null -eq $Root) { + if ($null -eq $Root) { $Root = @{ - Label = Get-RandomString + Label = Get-RandomString Children = @() } } $children = Get-Random -Minimum $MinChildren -Maximum $MaxChildren - for($i = 0; $i -lt $children; $i++) { + for ($i = 0; $i -lt $children; $i++) { $newChild = @{ - Label = Get-RandomString + Label = Get-RandomString Children = @() } $newTree = Get-RandomTree -Root $newChild -MaxChildren $MaxChildren -MaxDepth $MaxDepth -CurrentDepth $CurrentDepth @@ -151,7 +151,8 @@ function Get-AnsiEscapeSequence { $Escaped = $String.EnumerateRunes() | ForEach-Object { if ($_.Value -le 0x1f) { [Text.Rune]::new($_.Value + 0x2400) - } else { + } + else { $_ } } | Join-String @@ -162,6 +163,11 @@ function Get-AnsiEscapeSequence { } } } +function StripAnsi { + process { + [System.Management.Automation.Host.PSHostUserInterface]::GetOutputString($_, $false) + } +} function Get-PSStyleRandom { param( @@ -213,3 +219,18 @@ Function Get-SpectreColorSample { } } } +function Get-SpectreTableRowData { + param( + [int]$Count = 5, + [Switch]$Markup + ) + if ($null -eq $count) { + $count = 5 + } + 1..$Count | ForEach-Object { + if ($Markup) { + return '[{0}] {1} [/]' -f (Get-RandomColor), (Get-RandomString) + } + Get-RandomString + } +} diff --git a/PwshSpectreConsole.Tests/formatting/Format-SpectreTable.tests.ps1 b/PwshSpectreConsole.Tests/formatting/Format-SpectreTable.tests.ps1 index 2095528a..cae6d630 100644 --- a/PwshSpectreConsole.Tests/formatting/Format-SpectreTable.tests.ps1 +++ b/PwshSpectreConsole.Tests/formatting/Format-SpectreTable.tests.ps1 @@ -11,9 +11,9 @@ Describe "Format-SpectreTable" { Mock Write-AnsiConsole -Verifiable -ParameterFilter { $RenderableObject -is [Spectre.Console.Table] ` - -and ($testBorder -eq "None" -or $RenderableObject.Border.GetType().Name -like "*$testBorder*") ` - -and $RenderableObject.BorderStyle.Foreground.ToMarkup() -eq $testColor ` - -and $RenderableObject.Rows.Count -eq $testData.Count + -and ($testBorder -eq "None" -or $RenderableObject.Border.GetType().Name -like "*$testBorder*") ` + -and $RenderableObject.BorderStyle.Foreground.ToMarkup() -eq $testColor ` + -and $RenderableObject.Rows.Count -eq $testData.Count } } @@ -33,14 +33,15 @@ Describe "Format-SpectreTable" { It "Should be able to retrieve default display members for command output with format data" { $testData = Get-ChildItem "$PSScriptRoot" - $defaultDisplayMembers = $testData | Get-DefaultDisplayMembers - if($IsLinux -or $IsMacOS) { + $defaultDisplayMembers = $testData | Format-Table | Get-TableHeader + if ($IsLinux -or $IsMacOS) { # Expected @('UnixMode', 'User', 'Group', 'LastWrite…', 'Size', 'Name'), but got @('UnixMode', 'User', 'Group', 'LastWriteTime', 'Size', 'Name'). # i have no idea whats truncating LastWriteTime # $defaultDisplayMembers.Properties.GetEnumerator().Name | Should -Be @("UnixMode", "User", "Group", "LastWriteTime", "Size", "Name") - $defaultDisplayMembers.Properties.GetEnumerator().Name | Should -Match 'UnixMode|User|Group|LastWrite|Size|Name' - } else { - $defaultDisplayMembers.Properties.GetEnumerator().Name | Should -Be @("Mode", "LastWriteTime", "Length", "Name") + $defaultDisplayMembers.keys | Should -Match 'UnixMode|User|Group|LastWrite|Size|Name' + } + else { + $defaultDisplayMembers.keys | Should -Be @("Mode", "LastWriteTime", "Length", "Name") } } @@ -48,14 +49,14 @@ Describe "Format-SpectreTable" { { $defaultDisplayMembers = [hashtable]@{ "Hello" = "World" - } | Get-DefaultDisplayMembers + } | Get-TableHeader $defaultDisplayMembers | Should -Be $null } | Should -Not -Throw } It "Should be able to format ansi strings" { - $rawString = "hello world" - $ansiString = "`e[31mhello `e[46mworld`e[0m" + $rawString = "hello world" + $ansiString = "`e[31mhello `e[46mworld`e[0m" $result = ConvertTo-SpectreDecoration -String $ansiString $result.Length | Should -Be $rawString.Length } @@ -74,56 +75,48 @@ Describe "Format-SpectreTable" { } It "Should be able to format strings with spectre markup when opted in" { - $rawString = "hello spectremarkup world" - $ansiString = "hello [red]spectremarkup[/] world" + $rawString = "hello spectremarkup world" + $ansiString = "hello [red]spectremarkup[/] world" $result = ConvertTo-SpectreDecoration -String $ansiString -AllowMarkup $result.Length | Should -Be $rawString.Length } It "Should leave spectre markup alone by default" { - $ansiString = "hello [red]spectremarkup[/] world" + $ansiString = "hello [red]spectremarkup[/] world" $result = ConvertTo-SpectreDecoration -String $ansiString $result.Length | Should -Be $ansiString.Length } It "Should be able to create a new table cell with spectre markup" { - $rawString = "hello spectremarkup world" - $ansiString = "hello [red]spectremarkup[/] world" + $rawString = "hello spectremarkup world" + $ansiString = "hello [red]spectremarkup[/] world" $result = New-TableCell -String $ansiString -AllowMarkup $result | Should -BeOfType [Spectre.Console.Markup] $result.Length | Should -Be $rawString.Length } It "Should be able to create a new table cell without spectre markup by default" { - $ansiString = "hello [red]spectremarkup[/] world" + $ansiString = "hello [red]spectremarkup[/] world" $result = New-TableCell -String $ansiString $result | Should -BeOfType [Spectre.Console.Text] $result.Length | Should -Be $ansiString.Length } It "Should be able to create a new table row with spectre markup" { - $rawString = "Markup" - $entryItem = [pscustomobject]@{ - "Markup" = "[red]Markup[/]" - "Also" = "Hello" - } + $entryitem = Get-SpectreTableRowData -Markup $result = New-TableRow -Entry $entryItem -AllowMarkup $result -is [array] | Should -Be $true $result[0] | Should -BeOfType [Spectre.Console.Markup] - $result[0].Length | Should -Be $rawString.Length - $result.Count | Should -Be ($entryItem.PSObject.Properties | Measure-Object).Count + $result.Count | Should -Be $entryitem.Count } It "Should be able to create a new table row without spectre markup by default" { - $entryItem = [pscustomobject]@{ - "Markup" = "[red]Markup[/]" - "Also" = "Hello" - } + $entryitem = Get-SpectreTableRowData -Markup $result = New-TableRow -Entry $entryItem $result -is [array] | Should -Be $true $result[0] | Should -BeOfType [Spectre.Console.Text] - $result[0].Length | Should -Be $entryItem.Markup.Length - $result.Count | Should -Be ($entryItem.PSObject.Properties | Measure-Object).Count + $result[0].Length | Should -Be $entryItem[0].Length + $result.Count | Should -Be $entryitem.Count } } } diff --git a/PwshSpectreConsole.Tests/formatting/new_Format-SpectreJson.tests.ps1 b/PwshSpectreConsole.Tests/formatting/new_Format-SpectreJson.tests.ps1 new file mode 100644 index 00000000..70c88df7 --- /dev/null +++ b/PwshSpectreConsole.Tests/formatting/new_Format-SpectreJson.tests.ps1 @@ -0,0 +1,63 @@ +Remove-Module PwshSpectreConsole -Force -ErrorAction SilentlyContinue +Import-Module "$PSScriptRoot\..\..\PwshSpectreConsole\PwshSpectreConsole.psd1" -Force +Import-Module "$PSScriptRoot\..\TestHelpers.psm1" -Force + +Describe "Format-SpectreJson" { + InModuleScope "PwshSpectreConsole" { + BeforeEach { + $testData = $null + $testBorder = 'Markdown' + $testColor = Get-RandomColor + Mock Write-AnsiConsole { + param( + [Parameter(Mandatory)] + [Spectre.Console.Rendering.Renderable] $RenderableObject + ) + try { + $writer = [System.IO.StringWriter]::new() + $output = [Spectre.Console.AnsiConsoleOutput]::new($writer) + $settings = [Spectre.Console.AnsiConsoleSettings]::new() + $settings.Out = $output + $console = [Spectre.Console.AnsiConsole]::Create($settings) + $console.Write($RenderableObject) + $writer.ToString() + } + finally { + $writer.Dispose() + } + } + } + It "Simple scalar array test" { + { + $numbers = Get-Random -Minimum 30 -Maximum 50 + $json = 1..$numbers | Format-SpectreJson -Border None + ($json.trim() -split "\r?\n").count | Should -Be ($numbers + 2) # 10 items + 2 braces + } | Should -Not -Throw + } + It "Simple String test" { + { + $numbers = Get-Random -Minimum 30 -Maximum 50 + $json = 1..$numbers | ConvertTo-Json | Format-SpectreJson -Border None + ($json.trim() -split "\r?\n").count | Should -Be ($numbers + 2) # 10 items + 2 braces + } | Should -Not -Throw + } + It "Should take json string input" { + $data = @( + [pscustomobject]@{Name = "John"; Age = 25; City = "New York" }, + [pscustomobject]@{Name = "Jane"; Age = $null; City = "Los Angeles" } + ) + $roundtrip = $data | ConvertTo-Json | Format-SpectreJson -Border None | StripAnsi | ConvertFrom-Json + (Compare-Object -ReferenceObject $data -DifferenceObject $roundtrip -Property Name, Age, City -CaseSensitive -IncludeEqual).SideIndicator | Should -Be @('==','==') + } + It "Should roundtrip json string input" { + $ht = @{} + Get-RandomList -MinItems 30 -MaxItems 50 | ForEach-Object { + $ht[$_] = Get-RandomString + } + $data = [pscustomobject]$ht + $roundtrip = $data | ConvertTo-Json | Format-SpectreJson -Border None | StripAnsi | ConvertFrom-Json + $roundtrip.psobject.properties.name | Should -Be $data.psobject.properties.name + $roundtrip.psobject.properties.value | Should -Be $data.psobject.properties.value + } + } +} diff --git a/PwshSpectreConsole.Tests/formatting/new_Format-SpectreTable.tests.ps1 b/PwshSpectreConsole.Tests/formatting/new_Format-SpectreTable.tests.ps1 index 5a9e99b3..c3ac3693 100644 --- a/PwshSpectreConsole.Tests/formatting/new_Format-SpectreTable.tests.ps1 +++ b/PwshSpectreConsole.Tests/formatting/new_Format-SpectreTable.tests.ps1 @@ -27,26 +27,48 @@ Describe "Format-SpectreTable" { } } } - It "Should create a table when default display members for a command are required" { + It "Should create a table and display results properly" { $testData = Get-ChildItem "$PSScriptRoot" - $verification = Get-DefaultDisplayMembers $testData + $verification = $testdata | Format-Table | Get-TableHeader $testResult = Format-SpectreTable -Data $testData -Border $testBorder -Color $testColor - $command = Get-Command "Select-Object" - $rows = $testResult -split "\r?\n" | Select-Object -Skip 1 | Select-Object -SkipLast 2 + $rows = $testResult -split "\r?\n" | Select-Object -Skip 1 -SkipLast 2 $header = $rows[0] - $properties = $header -split '\|' | Get-AnsiEscapeSequence | ForEach-Object { - if (-Not [String]::IsNullOrWhiteSpace($_.Clean)) { - $_.Clean -replace '\s+' + $properties = $header -split '\|' | StripAnsi | ForEach-Object { + if (-Not [String]::IsNullOrWhiteSpace($_)) { + $_.Trim() } } if ($IsLinux -or $IsMacOS) { - $verification.Properties.keys | Should -Match 'UnixMode|User|Group|LastWrite|Size|Name' + $verification.keys | Should -Match 'UnixMode|User|Group|LastWrite|Size|Name' } else { - $verification.Properties.keys | Should -Be $properties + $verification.keys | Should -Be $properties } Assert-MockCalled -CommandName "Write-AnsiConsole" -Times 1 -Exactly Should -InvokeVerifiable } + It "Should create a table and display ICollection results properly" { + $testData = 1 | Group-Object + $testResult = Format-SpectreTable -Data $testData -Border Markdown -HideHeaders -Property Group + $clean = $testResult -replace '\s+|\|' + $clean | StripAnsi | Should -Be '{1}' + Assert-MockCalled -CommandName "Write-AnsiConsole" -Times 1 -Exactly + Should -InvokeVerifiable + } + It "Should be able to use calculated properties" { + $Data = Get-Process -Id $pid + $Format = $data | Format-SpectreTable ProcessName, @{Label="TotalRunningTime"; Expression={(Get-Date) - $_.StartTime}} -Border Markdown + $obj = $Format -split "\r?\n" | Select-Object -Skip 1 -SkipLast 2 + $deconstructed = $obj -split '\|' | StripAnsi | ForEach-Object { + if (-Not [String]::IsNullOrEmpty($_)) { + $_.Trim() + } + } + $deconstructed[0] | Should -Be 'ProcessName' + $deconstructed[1] | Should -Be 'TotalRunningTime' + $deconstructed[4] | Should -Be 'pwsh' + Assert-MockCalled -CommandName "Write-AnsiConsole" -Times 1 -Exactly + Should -InvokeVerifiable + } } } diff --git a/PwshSpectreConsole/private/Add-TableColumns.ps1 b/PwshSpectreConsole/private/Add-TableColumns.ps1 index 912681e2..4361965b 100644 --- a/PwshSpectreConsole/private/Add-TableColumns.ps1 +++ b/PwshSpectreConsole/private/Add-TableColumns.ps1 @@ -4,23 +4,27 @@ function Add-TableColumns { [CmdletBinding()] param( [Parameter(Mandatory)] - $table, - [Parameter(Mandatory)] - $Object, - [Collections.Specialized.OrderedDictionary] - $FormatData, - [String[]] - $Property, - [String] - $Title + [Table] $table, + [Collections.Specialized.OrderedDictionary] $FormatData, + [String] $Title, + [Switch] $Scalar, + [Switch] $Wrap ) Write-Debug "Module: $($ExecutionContext.SessionState.Module.Name) Command: $($MyInvocation.MyCommand.Name) Param: $($PSBoundParameters.GetEnumerator())" - if ($Property) { - Write-Debug 'Adding column from property' - foreach ($prop in $Property) { - $table.AddColumn($prop) | Out-Null + if ($Scalar) { + if ($Title) { + Write-Debug "Adding column with title: $Title" + $table.AddColumn($Title) | Out-Null + } + else { + Write-Debug "Adding column with title: Value" + $table.AddColumn("Value") | Out-Null + } + if (-Not $Wrap) { + $table.Columns[-1].NoWrap = $true } - } elseif ($FormatData) { + } + else { foreach ($key in $FormatData.keys) { $lookup = $FormatData[$key] Write-Debug "Adding column from formatdata: $($lookup.GetEnumerator())" @@ -33,22 +37,10 @@ function Add-TableColumns { if ($lookup.Alignment -ne 'undefined') { $table.Columns[-1].Alignment = [Justify]::$lookup.Alignment } - } - } elseif (Test-IsScalar $Object) { - # simple/scalar types show up wonky, we can detect them and just use a dummy header for the table - Write-Debug 'simple/scalar type' - $script:scalarDetected = $true - if ($Title) { - $table.AddColumn($Title) | Out-Null - } else { - $table.AddColumn("Value") | Out-Null - } - } else { - # no formatting found and no properties selected, enumerating psobject.properties.name - Write-Debug 'PSCustomObject/Properties switch detected' - foreach ($prop in $Object.psobject.Properties.Name) { - if (-Not [String]::IsNullOrEmpty($prop)) { - $table.AddColumn($prop) | Out-Null + if (-Not $Wrap) { + # https://github.com/spectreconsole/spectre.console/issues/1185 + # leaving it in as it will probably get fixed, has no effect on output yet. + $table.Columns[-1].NoWrap = $true } } } diff --git a/PwshSpectreConsole/private/Get-DefaultDisplayMembers.ps1 b/PwshSpectreConsole/private/Get-DefaultDisplayMembers.ps1 deleted file mode 100644 index a3e8307f..00000000 --- a/PwshSpectreConsole/private/Get-DefaultDisplayMembers.ps1 +++ /dev/null @@ -1,53 +0,0 @@ -function Get-DefaultDisplayMembers { - <# - .SYNOPSIS - Get the default display members for an object using the formatdata. - .NOTES - rewrite, borrowed some code from chrisdents gist. - .LINK - https://raw.githubusercontent.com/PowerShell/GraphicalTools/master/src/Microsoft.PowerShell.ConsoleGuiTools/TypeGetter.cs - https://gist.github.com/indented-automation/834284b6c904339b0454199b4745237e - - #> - param( - [Parameter(Mandatory, ValueFromPipeline)] - [Object]$Object - ) - try { - Write-Debug "getting formatdata for $($Object[0].PSTypeNames)" - $formatData = Get-FormatData -TypeName $Object[0].PSTypeNames | Select-Object -First 1 - Write-Debug "formatData: $($formatData.count)" - } catch { - # error getting formatdata, return null - return $null - } - if (-Not $formatData) { - # no formatdata, return null - return $null - } - # this needs to ordered to preserve table column order. - $properties = [ordered]@{} - $viewDefinition = $formatData.FormatViewDefinition | Where-Object { $_.Control -match 'TableControl' } | Select-Object -First 1 - Write-Debug "viewDefinition: $($viewDefinition.Name)" - $format = for ($i = 0; $i -lt $viewDefinition.Control.Headers.Count; $i++) { - $name = $viewDefinition.Control.Headers[$i].Label - $displayEntry = $viewDefinition.Control.Rows.Columns[$i].DisplayEntry - if (-not $name) { - $name = $displayEntry.Value - } - $expression = switch ($displayEntry.ValueType) { - 'Property' { $displayEntry.Value } - 'ScriptBlock' { [ScriptBlock]::Create($displayEntry.Value) } - } - $properties[$name] = @{ - Label = $name - Width = $viewDefinition.Control.headers[$i].width - Alignment = $viewDefinition.Control.headers[$i].alignment - } - @{ Name = $name; Expression = $expression } - } - return [PSCustomObject]@{ - Properties = $properties - Format = $format - } -} diff --git a/PwshSpectreConsole/private/Get-TableHeader.ps1 b/PwshSpectreConsole/private/Get-TableHeader.ps1 new file mode 100644 index 00000000..69ab4f7c --- /dev/null +++ b/PwshSpectreConsole/private/Get-TableHeader.ps1 @@ -0,0 +1,40 @@ +function Get-TableHeader { + <# + ls | ft | Get-TableHeader + https://gist.github.com/Jaykul/9999be71ee68f3036dc2529c451729f4 + #> + [CmdletBinding()] + param( + [Parameter(ValueFromPipeline)] + $FormatStartData + ) + begin { + Write-Debug "Module: $($ExecutionContext.SessionState.Module.Name) Command: $($MyInvocation.MyCommand.Name) Param: $($PSBoundParameters.GetEnumerator())" + $alignment = @{ + 0 = 'undefined' + 1 = 'Left' + 2 = 'Center' + 3 = 'Right' + } + } + process { + if ($FormatStartData.Gettype().Name -eq 'FormatStartData') { + $properties = [ordered]@{} + $FormatStartData.shapeinfo.tablecolumninfolist | Where-Object { $_ } | ForEach-Object { + $Name = $_.Label ? $_.Label : $_.propertyName + $properties[$Name] = @{ + Label = $Name + Width = $_.width + Alignment = $alignment.Contains($_.alignment) ? $alignment[$_.alignment] : 'undefined' + HeaderMatchesProperty = $_.HeaderMatchesProperty + # PropertyName = $_.propertyName + } + } + if ($properties.Keys.Count -eq 0) { + Write-Debug "No properties found" + return $null + } + return $properties + } + } +} diff --git a/PwshSpectreConsole/private/New-TableCell.ps1 b/PwshSpectreConsole/private/New-TableCell.ps1 index 08165f7b..23be0ea7 100644 --- a/PwshSpectreConsole/private/New-TableCell.ps1 +++ b/PwshSpectreConsole/private/New-TableCell.ps1 @@ -1,8 +1,8 @@ function New-TableCell { [cmdletbinding()] param( - $String, - [Switch]$AllowMarkup + [Object] $String, + [Switch] $AllowMarkup ) Write-Debug "Module: $($ExecutionContext.SessionState.Module.Name) Command: $($MyInvocation.MyCommand.Name) Param: $($PSBoundParameters.GetEnumerator())" if ([String]::IsNullOrEmpty($String)) { diff --git a/PwshSpectreConsole/private/New-TableRow.ps1 b/PwshSpectreConsole/private/New-TableRow.ps1 index ce6514f5..e7ba25fc 100644 --- a/PwshSpectreConsole/private/New-TableRow.ps1 +++ b/PwshSpectreConsole/private/New-TableRow.ps1 @@ -1,46 +1,30 @@ function New-TableRow { param( - $Entry, - [Switch] $FormatFound, - [Switch] $PropertiesSelected, - [Switch] $AllowMarkup + [Parameter(Mandatory)] + [Object] $Entry, + [Switch] $AllowMarkup, + [Switch] $Scalar ) Write-Debug "Module: $($ExecutionContext.SessionState.Module.Name) Command: $($MyInvocation.MyCommand.Name) Param: $($PSBoundParameters.GetEnumerator())" - $opts = @{} - if ($AllowMarkup) { - $opts.AllowMarkup = $true + $opts = @{ + AllowMarkup = $AllowMarkup } - if ((-Not $FormatFound -or -Not $PropertiesSelected) -And ($scalarDetected -eq $true)) { + if ($scalar) { New-TableCell -String $Entry @opts } else { # simplified, should be faster. $detectVT = '\x1b' - $rows = foreach ($cell in $Entry.psobject.Properties) { - if ([String]::IsNullOrEmpty($cell.Value)) { + $rows = foreach ($cell in $Entry) { + if ([String]::IsNullOrEmpty($cell)) { New-TableCell @opts continue } - if ($cell.value -match $detectVT) { - if ($FormatFound) { - # we are dealing with an object that has VT codes and a formatdata entry. - # this returns a spectre.console.text/markup object with the VT codes applied. - ConvertTo-SpectreDecoration -String $cell.Value @opts - continue - } - else { - # we are dealing with an object that has VT codes but no formatdata entry. - # this returns a string with the VT codes stripped. - # we could pass it to ConvertTo-SpectreDecoration, should we? - # note if multiple colors are used it will only use the last color. - # better to use Markup to manually add colors. - Write-Debug "VT codes detected, but no formatdata entry. stripping VT codes, preferred method of manually adding colors is markup" - New-TableCell -String ([System.Management.Automation.Host.PSHostUserInterface]::GetOutputString($cell.Value, $false)) @opts - # ConvertTo-SpectreDecoration -String $cell.Value @opts - continue - } + if ($cell -match $detectVT) { + ConvertTo-SpectreDecoration -String $cell @opts + continue } - New-TableCell -String $cell.Value @opts + New-TableCell -String $cell @opts } return $rows } diff --git a/PwshSpectreConsole/private/Test-IsScalar.ps1 b/PwshSpectreConsole/private/Test-IsScalar.ps1 index 920eee90..aff093ef 100644 --- a/PwshSpectreConsole/private/Test-IsScalar.ps1 +++ b/PwshSpectreConsole/private/Test-IsScalar.ps1 @@ -1,13 +1,11 @@ function Test-IsScalar { [CmdletBinding()] - param ( + param( $Value ) Write-Debug "Module: $($ExecutionContext.SessionState.Module.Name) Command: $($MyInvocation.MyCommand.Name) Param: $($PSBoundParameters.GetEnumerator())" if ($Value -is [System.Collections.IEnumerable] -and $Value -isnot [string]) { - $firstItem = $Value | Select-Object -First 1 - return $firstItem -is [System.ValueType] -or $firstItem -is [System.String] - } else { - return $Value -is [System.ValueType] -or $Value -is [System.String] + $Value = $Value | Select-Object -First 1 } + return $Value -is [System.ValueType] -or $Value -is [System.String] } diff --git a/PwshSpectreConsole/private/completions/Completers.psm1 b/PwshSpectreConsole/private/completions/Completers.psm1 index a2684e5e..099afe75 100644 --- a/PwshSpectreConsole/private/completions/Completers.psm1 +++ b/PwshSpectreConsole/private/completions/Completers.psm1 @@ -62,3 +62,18 @@ class SpectreConsoleTreeGuide : IValidateSetValuesGenerator { return $lookup } } +class ColorTransformationAttribute : ArgumentTransformationAttribute { + [object] Transform([EngineIntrinsics]$engine, [object]$inputData) { + if ($InputData -is [Color]) { + return $InputData + } + if ($InputData.StartsWith('#')) { + $hexBytes = [System.Convert]::FromHexString($InputData.Substring(1)) + return [Color]::new($hexBytes[0], $hexBytes[1], $hexBytes[2]) + } + if ($InputData -is [String]) { + return [Color]::$InputData + } + throw [System.ArgumentException]::new("Cannot convert '$InputData' to [Spectre.Console.Color]") + } +} diff --git a/PwshSpectreConsole/public/formatting/Format-SpectreJson.ps1 b/PwshSpectreConsole/public/formatting/Format-SpectreJson.ps1 index fb152459..c9e71c1f 100644 --- a/PwshSpectreConsole/public/formatting/Format-SpectreJson.ps1 +++ b/PwshSpectreConsole/public/formatting/Format-SpectreJson.ps1 @@ -1,9 +1,10 @@ using module "..\..\private\completions\Completers.psm1" +using namespace Spectre.Console function Format-SpectreJson { <# .SYNOPSIS - Formats an array of objects into a Spectre Console Json. + Formats an array of objects into a Spectre Console Json. Thanks to [trackd](https://github.com/trackd) for adding this. ![Spectre json example](/json.png) @@ -63,7 +64,7 @@ function Format-SpectreJson { #> [Reflection.AssemblyMetadata("title", "Format-SpectreJson")] [Alias('fsj')] - param ( + param( [Parameter(ValueFromPipeline, Mandatory)] [object] $Data, [int] $Depth, @@ -71,9 +72,9 @@ function Format-SpectreJson { [switch] $NoBorder, [ValidateSet([SpectreConsoleBoxBorder], ErrorMessage = "Value '{0}' is invalid. Try one of: {1}")] [string] $Border = "Rounded", - [ValidateSpectreColor()] + [ColorTransformationAttribute()] [ArgumentCompletionsSpectreColors()] - [string] $Color = $script:AccentColor.ToMarkup(), + [Color] $Color = $script:AccentColor, [ValidateScript({ $_ -gt 0 -and $_ -le (Get-HostWidth) }, ErrorMessage = "Value '{0}' is invalid. Cannot be negative or exceed console width.")] [int] $Width, [ValidateScript({ $_ -gt 0 -and $_ -le (Get-HostHeight) }, ErrorMessage = "Value '{0}' is invalid. Cannot be negative or exceed console height.")] @@ -83,17 +84,82 @@ function Format-SpectreJson { begin { $collector = [System.Collections.Generic.List[psobject]]::new() $splat = @{ - WarningAction = "Ignore" + WarningAction = 'Ignore' + ErrorAction = 'Stop' } if ($Depth) { $splat.Depth = $Depth } + $ht = [ordered]@{} } process { - $collector.add($data) + if ($MyInvocation.ExpectingInput) { + if ($data -is [string]) { + if ($data.pschildname) { + if (-Not $ht.contains($data.pschildname)) { + $ht[$data.pschildname] = [System.Text.StringBuilder]::new() + } + return [void]$ht[$data.pschildname].AppendLine($data) + } + # assume we get the entire json in one go a string (e.g -Raw or invoke-webrequest) + try { + $jsonObjects = $data | Out-String | ConvertFrom-Json -AsHashtable @splat + return $collector.add($jsonObjects) + } + catch { + Write-Debug "Failed to convert string to object, $_" + } + } + if ($data -is [System.IO.FileSystemInfo]) { + if ($data.Extension -eq '.json') { + Write-Debug "json file found, reading $($data.FullName)" + try { + $jsonObjects = Get-Content -Raw $data.FullName | ConvertFrom-Json -AsHashtable @splat + return $collector.add($jsonObjects) + } + catch { + Write-Debug "Failed to convert json to object, $_" + } + } + return $collector.add( + [pscustomobject]@{ + Name = $data.Name + FullName = $data.FullName + Type = $data.GetType().Name.TrimEnd('Info') + }) + } + Write-Debug "adding item from pipeline" + return $collector.add($data) + } + foreach ($item in $data) { + Write-Debug "adding item from input" + $collector.add($item) + } } end { - $json = [Spectre.Console.Json.JsonText]::new(($collector | ConvertTo-Json @splat)) + if ($ht.keys.count -gt 0) { + foreach ($key in $ht.Keys) { + Write-Debug "converting json stream to object, $key" + try { + $jsonObject = $ht[$key].ToString() | Out-String | ConvertFrom-Json -AsHashtable @splat + $collector.add($jsonObject) + continue + } + catch { + Write-Debug "Failed to convert json to object: $key, $_" + } + } + } + if ($collector.Count -eq 0) { + return + } + try { + $json = [Spectre.Console.Json.JsonText]::new(($collector | ConvertTo-Json @splat)) + } + catch { + Write-Error "Failed to convert to json, $_" + return + } $json.BracesStyle = [Spectre.Console.Style]::new([Spectre.Console.Color]::Red) $json.BracketsStyle = [Spectre.Console.Style]::new([Spectre.Console.Color]::Green) $json.ColonStyle = [Spectre.Console.Style]::new([Spectre.Console.Color]::Blue) @@ -103,14 +169,14 @@ function Format-SpectreJson { $json.BooleanStyle = [Spectre.Console.Style]::new([Spectre.Console.Color]::Teal) $json.NullStyle = [Spectre.Console.Style]::new([Spectre.Console.Color]::Plum1) - if($NoBorder) { + if ($NoBorder) { Write-AnsiConsole $json return } $panel = [Spectre.Console.Panel]::new($json) $panel.Border = [Spectre.Console.BoxBorder]::$Border - $panel.BorderStyle = [Spectre.Console.Style]::new(($Color | Convert-ToSpectreColor)) + $panel.BorderStyle = [Spectre.Console.Style]::new($Color) if ($Title) { $panel.Header = [Spectre.Console.PanelHeader]::new($Title) } @@ -120,7 +186,7 @@ function Format-SpectreJson { if ($height) { $panel.Height = $Height } - if($Expand) { + if ($Expand) { $panel.Expand = $Expand } Write-AnsiConsole $panel diff --git a/PwshSpectreConsole/public/formatting/Format-SpectreTable.ps1 b/PwshSpectreConsole/public/formatting/Format-SpectreTable.ps1 index b2bbbeda..baeb9742 100644 --- a/PwshSpectreConsole/public/formatting/Format-SpectreTable.ps1 +++ b/PwshSpectreConsole/public/formatting/Format-SpectreTable.ps1 @@ -11,10 +11,21 @@ function Format-SpectreTable { This function takes an array of objects and formats them into a table using the Spectre Console library. The table can be customized with a border style and color. .PARAMETER Property - The list of properties to select for the table from the input data. + Specifies the object properties that appear in the display and the order in which they appear. + Type one or more property names, separated by commas, or use a hash table to display a calculated property. + Wildcards are permitted. + The Property parameter is optional. You can't use the Property and View parameters in the same command. + The value of the Property parameter can be a new calculated property. + The calculated property can be a script block or a hash table. Valid key-value pairs are: + - Name (or Label) `` + - Expression - `` or `