Skip to content

Commit

Permalink
Loads of changes for powershell built-in formatters and switch to ret…
Browse files Browse the repository at this point in the history
…urning renderables instead of always outputting to ansi console to allow nesting
  • Loading branch information
ShaunLawrie committed Aug 11, 2024
1 parent 5d8c175 commit 9da6338
Show file tree
Hide file tree
Showing 32 changed files with 1,573 additions and 1,173 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Upgrading to 1.0

I started this as a learning excercise in how to bridge the gap between C# libraries and PowerShell and wow have I learned a lot. Things I thought made sense when I first wrote this have now made it difficult to maintain. I've tried to maintain as much backwards compatibility as I can but there are some areas which will have breaking changes when upgrading to 1.0.

## New Features

- Renderable items use PowerShell formatters (thanks @startautomating) so you can now assign the output of functions like `Format-SpectreJson` to a variable and use it inside other Spectre Console functions like `Format-SpectreTable`.

## Changes

- Parameter names for a lot of commandlets have been aligned with the terminology in Spectre.Console, this affects commands all throughout this module but to maintain backwards compatibility the old parameter names have been kept as aliases so existing scripts will continue to work. Exceptions to this are:
- Format-SpectreJson parameters removed are `-Border`, `-Title`, `-NoBorder`. To wrap the json in a border the suggested option is to pipe the output to Format-SpectrePanel e.g. `Format-SpectreJson -Data $data | Format-SpectrePanel`
- TODO find the others
16 changes: 12 additions & 4 deletions PwshSpectreConsole/Build.ps1
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
param (
[string] $Version = "0.49.0"
[string] $Version = "0.49.1",
[int] $DotnetSdkMajorVersion = 6
)

function Install-SpectreConsole {
Expand Down Expand Up @@ -53,9 +54,14 @@ function Install-SpectreConsole {

$command = Get-Command "dotnet" -ErrorAction SilentlyContinue
if ($null -eq $command) {
throw "dotnet not found, please install dotnet sdk 6"
} elseif (-not (dotnet --list-sdks | Select-String "^6.+")) {
throw "dotnet sdk 6 not found, please install dotnet sdk 6"
throw "dotnet not found, please install dotnet sdk $DotnetSdkMajorVersion"
} elseif (-not (dotnet --list-sdks | Select-String "^$DotnetSdkMajorVersion.+")) {
Write-Warning "dotnet sdk $DotnetSdkMajorVersion not found, please install dotnet sdk $DotnetSdkMajorVersion"
if (Get-Command "winget" -ErrorAction SilentlyContinue) {
winget install "Microsoft.DotNet.SDK.$DotnetSdkMajorVersion"
} else {
throw "Please install the dotnet sdk and try again"
}
}
try {
Push-Location
Expand All @@ -64,6 +70,8 @@ function Install-SpectreConsole {
} finally {
Pop-Location
}

& "$PSScriptRoot\PwshSpectreConsole.EzFormat.ps1"
}

Write-Host "Downloading Spectre.Console version $Version"
Expand Down
39 changes: 39 additions & 0 deletions PwshSpectreConsole/PwshSpectreConsole.EzFormat.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#requires -Module EZOut
# Install-Module EZOut or https://github.com/StartAutomating/EZOut
$myFile = $MyInvocation.MyCommand.ScriptBlock.File
$myModuleName = $($myFile | Split-Path -Leaf) -replace '\.ezformat\.ps1', '' -replace '\.ezout\.ps1', ''
$myRoot = $myFile | Split-Path
Push-Location $myRoot
$formatting = @(
# Add your own Write-FormatView here,
# or put them in a Formatting or Views directory
foreach ($potentialDirectory in 'Formatting','Views','Types') {
Join-Path $myRoot $potentialDirectory |
Get-ChildItem -ea ignore |
Import-FormatView -FilePath {$_.Fullname}
}
)

$destinationRoot = $myRoot

if ($formatting) {
$myFormatFilePath = Join-Path $destinationRoot "$myModuleName.format.ps1xml"
# You can also output to multiple paths by passing a hashtable to -OutputPath.
$formatting | Out-FormatData -Module $MyModuleName -OutputPath $myFormatFilePath
}

$types = @(
# Add your own Write-TypeView statements here
# or declare them in the 'Types' directory
Join-Path $myRoot Types |
Get-Item -ea ignore |
Import-TypeView

)

if ($types) {
$myTypesFilePath = Join-Path $destinationRoot "$myModuleName.types.ps1xml"
# You can also output to multiple paths by passing a hashtable to -OutputPath.
$types | Out-TypeData -OutputPath $myTypesFilePath
}
Pop-Location
46 changes: 46 additions & 0 deletions PwshSpectreConsole/PwshSpectreConsole.format.ps1xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<!-- Generated with EZOut 2.0.6: Install-Module EZOut or https://github.com/StartAutomating/EZOut -->
<Configuration>
<ViewDefinitions>
<View>
<Name>Spectre.Console.Rendering.Renderable</Name>
<ViewSelectedBy>
<TypeName>Spectre.Console.Rendering.Renderable</TypeName>
</ViewSelectedBy>
<CustomControl>
<CustomEntries>
<CustomEntry>
<CustomItem>
<ExpressionBinding>
<ScriptBlock>
# Work out if the current object is being piped to another command, there isn't access to the pipeline in the format view script block so it's using a janky regex workaround
try {
$line = $MyInvocation.Line
$start = $MyInvocation.OffsetInLine
$lineAfterOffset = $line.SubString($start, ($line.Length - $start))
$targetIsInPipeline = $lineAfterOffset | Select-String "^[^;]+?\|"
$pipelineSegment = $lineAfterOffset | Select-String "^[^;]+?(;|$)" | Select-Object -ExpandProperty Matches -First 1 | Select-Object -ExpandProperty Value
$targetIsPipedToSpectreFunction = $pipelineSegment -match ".*\|.*(Write|Format|Out)-Spectre.*"
Write-Debug "Line: $line"
Write-Debug "Start: $start"
Write-Debug "Line after offset: $lineAfterOffset"
Write-Debug "Target is in pipeline: $targetIsInPipeline"
Write-Debug "Pipeline segment: $pipelineSegment"
Write-Debug "Target is piped to Spectre function: $targetIsPipedToSpectreFunction"
} catch {
Write-Debug "Failed to discover pipeline state for Spectre.Console.Rendering.Renderable: $_"
}

if ($targetIsInPipeline -and -not $targetIsPipedToSpectreFunction) {
$_
} else {
Write-AnsiConsole $_
}
</ScriptBlock>
</ExpressionBinding>
</CustomItem>
</CustomEntry>
</CustomEntries>
</CustomControl>
</View>
</ViewDefinitions>
</Configuration>
5 changes: 3 additions & 2 deletions PwshSpectreConsole/PwshSpectreConsole.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ RequiredAssemblies = '.\packages\Spectre.Console\lib\net6.0\Spectre.Console.dll'
# TypesToProcess = @()

# Format files (.ps1xml) to be loaded when importing this module
# FormatsToProcess = @()
FormatsToProcess = 'PwshSpectreConsole.format.ps1xml'

# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess
# NestedModules = @()
Expand All @@ -86,7 +86,8 @@ FunctionsToExport = 'Add-SpectreJob', 'Format-SpectreBarChart',
'New-SpectreChartItem', 'Invoke-SpectreScriptBlockQuietly',
'Get-SpectreDemoColors', 'Get-SpectreDemoEmoji', 'Format-SpectreJson',
'Write-SpectreCalendar', 'Start-SpectreRecording',
'Stop-SpectreRecording'
'Stop-SpectreRecording', 'Format-SpectreColumns', 'Write-AnsiConsole',
'Out-SpectreHost'

# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export.
CmdletsToExport = @()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# TODO - Ask @startautomating how this can be done better
Write-FormatView -TypeName "Spectre.Console.Rendering.Renderable" -Action {
# Work out if the current object is being piped to another command, there isn't access to the pipeline in the format view script block so it's using a janky regex workaround
try {
$line = $MyInvocation.Line
$start = $MyInvocation.OffsetInLine
$lineAfterOffset = $line.SubString($start, ($line.Length - $start))
$targetIsInPipeline = $lineAfterOffset | Select-String "^[^;]+?\|"
$pipelineSegment = $lineAfterOffset | Select-String "^[^;]+?(;|$)" | Select-Object -ExpandProperty Matches -First 1 | Select-Object -ExpandProperty Value
$targetIsPipedToSpectreFunction = $pipelineSegment -match ".*\|.*(Write|Format|Out)-Spectre.*"
Write-Debug "Line: $line"
Write-Debug "Start: $start"
Write-Debug "Line after offset: $lineAfterOffset"
Write-Debug "Target is in pipeline: $targetIsInPipeline"
Write-Debug "Pipeline segment: $pipelineSegment"
Write-Debug "Target is piped to Spectre function: $targetIsPipedToSpectreFunction"
} catch {
Write-Debug "Failed to discover pipeline state for Spectre.Console.Rendering.Renderable: $_"
}

if ($targetIsInPipeline -and -not $targetIsPipedToSpectreFunction) {
$_
} else {
Write-AnsiConsole $_
}
}
71 changes: 39 additions & 32 deletions PwshSpectreConsole/private/Add-SpectreTreeNode.ps1
Original file line number Diff line number Diff line change
@@ -1,33 +1,40 @@
using namespace Spectre.Console

<#
.SYNOPSIS
Recursively adds child nodes to a parent node in a Spectre.Console tree.
.DESCRIPTION
The Add-SpectreTreeNode function adds child nodes to a parent node in a Spectre.Console tree. It does this recursively, so it can handle nested child nodes.
.PARAMETER Node
The parent node to which the child nodes will be added.
.PARAMETER Children
An array of child nodes to be added to the parent node. Each child node should be an object with a 'Label' property and a 'Children' property (which can be an empty array if the child has no children of its own).
.NOTES
See Format-SpectreTree for usage.
#>
function Add-SpectreTreeNode {
param (
[Parameter(Mandatory)]
[IHasTreeNodes] $Node,
[Parameter(Mandatory)]
[array] $Children
)

foreach ($child in $Children) {
$newNode = [HasTreeNodeExtensions]::AddNode($Node, $child.Label)
if ($child.Children.Count -gt 0) {
Add-SpectreTreeNode -Node $newNode -Children $child.Children
}
}
using namespace Spectre.Console

<#
.SYNOPSIS
Recursively adds child nodes to a parent node in a Spectre.Console tree.
.DESCRIPTION
The Add-SpectreTreeNode function adds child nodes to a parent node in a Spectre.Console tree. It does this recursively, so it can handle nested child nodes.
.PARAMETER Node
The parent node to which the child nodes will be added.
.PARAMETER Children
An array of child nodes to be added to the parent node. Each child node should be an object with a 'Label' property and a 'Children' property (which can be an empty array if the child has no children of its own).
.NOTES
See Format-SpectreTree for usage.
#>
function Add-SpectreTreeNode {
param (
[Parameter(Mandatory)]
[IHasTreeNodes] $Node,
[Parameter(Mandatory)]
[array] $Children
)

foreach ($child in $Children) {

# Backwards compatibility: Value used to be called Label
if ($child.ContainsKey("Label")) {
$child["Value"] = $child["Label"]
$child.Remove("Label")
}

$newNode = [HasTreeNodeExtensions]::AddNode($Node, $child.Value)
if ($child.Children.Count -gt 0) {
Add-SpectreTreeNode -Node $newNode -Children $child.Children
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
function Convert-HashtableToRenderSafePSObject {
param(
[object] $Hashtable,
[hashtable] $Renderables
)
$customObject = @{}
foreach ($item in $Hashtable.GetEnumerator()) {
if ($item.Value -is [hashtable] -or $item.Value -is [ordered]) {
$item.Value = Convert-HashtableToRenderSafePSObject -Hashtable $item.Value
} elseif ($item.Value -is [Spectre.Console.Rendering.Renderable]) {
$renderableKey = "RENDERABLE__$([Guid]::NewGuid().Guid)"
$Renderables[$renderableKey] = $item.Value
$item.Value = $renderableKey
}
$customObject[$item.Key] = $item.Value
}
return [pscustomobject]$customObject
}
45 changes: 24 additions & 21 deletions PwshSpectreConsole/private/Write-AnsiConsole.ps1
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
using namespace Spectre.Console

<#
.SYNOPSIS
Writes an object to the console using [AnsiConsole]::Write()
.DESCRIPTION
This function is required for mocking ansiconsole in unit tests that write objects to the console.
.PARAMETER RenderableObject
The renderable object to write to the console e.g. [BarChart]
.EXAMPLE
Write-SpectreConsoleOutput -Object "Hello, World!" -ForegroundColor Green -BackgroundColor Black
#>
function Write-AnsiConsole {
param(
[Parameter(Mandatory)]
[Rendering.Renderable] $RenderableObject
)
[AnsiConsole]::Write($RenderableObject)
using module ".\completions\Transformers.psm1"
using namespace Spectre.Console

<#
.SYNOPSIS
Writes an object to the console using [AnsiConsole]::Write()
.DESCRIPTION
This function is required for mocking ansiconsole in unit tests that write objects to the console.
.PARAMETER RenderableObject
The renderable object to write to the console e.g. [BarChart]
.EXAMPLE
Write-SpectreConsoleOutput -Object "Hello, World!" -ForegroundColor Green -BackgroundColor Black
#>
function Write-AnsiConsole {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[RenderableTransformationAttribute()]
[object] $RenderableObject
)
[AnsiConsole]::Render($RenderableObject)
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Spectre.Console" Version="0.49.0" />
<PackageReference Include="Spectre.Console" Version="0.49.1" />
<PackageReference Include="System.Management.Automation" Version="6.*" PrivateAssets="all" />
</ItemGroup>

Expand Down
Loading

0 comments on commit 9da6338

Please sign in to comment.