From 9da6338770f47cc768188b924c41a9a1401beef6 Mon Sep 17 00:00:00 2001 From: Shaun Lawrie Date: Mon, 12 Aug 2024 00:46:18 +1200 Subject: [PATCH] Loads of changes for powershell built-in formatters and switch to returning renderables instead of always outputting to ansi console to allow nesting --- .../content/docs/guides/upgrading-to-1-0.md | 13 + PwshSpectreConsole/Build.ps1 | 16 +- .../PwshSpectreConsole.EzFormat.ps1 | 39 ++ .../PwshSpectreConsole.format.ps1xml | 46 ++ PwshSpectreConsole/PwshSpectreConsole.psd1 | 5 +- .../Spectre.Console.AnsiConsole.format.ps1 | 26 + .../private/Add-SpectreTreeNode.ps1 | 71 +- .../Convert-HashtableToRenderSafePSObject.ps1 | 18 + .../private/Write-AnsiConsole.ps1 | 45 +- .../private/classes/PwshSpectreConsole.csproj | 2 +- .../private/completions/Completers.psm1 | 187 ++--- .../private/completions/Transformers.psm1 | 90 +++ .../public/config/Set-SpectreColors.ps1 | 99 +-- .../public/demo/Start-SpectreDemo.ps1 | 662 +++++++++--------- .../formatting/Format-SpectreBarChart.ps1 | 133 ++-- .../Format-SpectreBreakdownChart.ps1 | 132 ++-- .../formatting/Format-SpectreColumns.ps1 | 54 ++ .../public/formatting/Format-SpectreJson.ps1 | 89 ++- .../public/formatting/Format-SpectrePanel.ps1 | 144 ++-- .../public/formatting/Format-SpectreTable.ps1 | 27 +- .../public/formatting/Format-SpectreTree.ps1 | 134 ++-- .../public/output/Out-SpectreHost.ps1 | 39 ++ .../Invoke-SpectreCommandWithStatus.ps1 | 117 ++-- .../public/prompts/Read-SpectreConfirm.ps1 | 141 ++-- .../prompts/Read-SpectreMultiSelection.ps1 | 1 + .../Read-SpectreMultiSelectionGrouped.ps1 | 1 + .../public/prompts/Read-SpectreSelection.ps1 | 1 + .../public/prompts/Read-SpectreText.ps1 | 143 ++-- ...Calender.ps1 => Write-SpectreCalendar.ps1} | 20 +- .../writing/Write-SpectreFigletText.ps1 | 97 +-- .../public/writing/Write-SpectreHost.ps1 | 75 +- .../public/writing/Write-SpectreRule.ps1 | 79 ++- 32 files changed, 1573 insertions(+), 1173 deletions(-) create mode 100644 PwshSpectreConsole.Docs/src/content/docs/guides/upgrading-to-1-0.md create mode 100644 PwshSpectreConsole/PwshSpectreConsole.EzFormat.ps1 create mode 100644 PwshSpectreConsole/PwshSpectreConsole.format.ps1xml create mode 100644 PwshSpectreConsole/formatting/Spectre.Console.AnsiConsole.format.ps1 create mode 100644 PwshSpectreConsole/private/Convert-HashtableToRenderSafePSObject.ps1 create mode 100644 PwshSpectreConsole/private/completions/Transformers.psm1 create mode 100644 PwshSpectreConsole/public/formatting/Format-SpectreColumns.ps1 create mode 100644 PwshSpectreConsole/public/output/Out-SpectreHost.ps1 rename PwshSpectreConsole/public/writing/{Write-SpectreCalender.ps1 => Write-SpectreCalendar.ps1} (82%) diff --git a/PwshSpectreConsole.Docs/src/content/docs/guides/upgrading-to-1-0.md b/PwshSpectreConsole.Docs/src/content/docs/guides/upgrading-to-1-0.md new file mode 100644 index 00000000..a9f9967c --- /dev/null +++ b/PwshSpectreConsole.Docs/src/content/docs/guides/upgrading-to-1-0.md @@ -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 diff --git a/PwshSpectreConsole/Build.ps1 b/PwshSpectreConsole/Build.ps1 index 4814a2e5..0ea83ee1 100644 --- a/PwshSpectreConsole/Build.ps1 +++ b/PwshSpectreConsole/Build.ps1 @@ -1,5 +1,6 @@ param ( - [string] $Version = "0.49.0" + [string] $Version = "0.49.1", + [int] $DotnetSdkMajorVersion = 6 ) function Install-SpectreConsole { @@ -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 @@ -64,6 +70,8 @@ function Install-SpectreConsole { } finally { Pop-Location } + + & "$PSScriptRoot\PwshSpectreConsole.EzFormat.ps1" } Write-Host "Downloading Spectre.Console version $Version" diff --git a/PwshSpectreConsole/PwshSpectreConsole.EzFormat.ps1 b/PwshSpectreConsole/PwshSpectreConsole.EzFormat.ps1 new file mode 100644 index 00000000..8b77e02e --- /dev/null +++ b/PwshSpectreConsole/PwshSpectreConsole.EzFormat.ps1 @@ -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 diff --git a/PwshSpectreConsole/PwshSpectreConsole.format.ps1xml b/PwshSpectreConsole/PwshSpectreConsole.format.ps1xml new file mode 100644 index 00000000..68f20b3b --- /dev/null +++ b/PwshSpectreConsole/PwshSpectreConsole.format.ps1xml @@ -0,0 +1,46 @@ + + + + + Spectre.Console.Rendering.Renderable + + Spectre.Console.Rendering.Renderable + + + + + + + + # 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 $_ + } + + + + + + + + + \ No newline at end of file diff --git a/PwshSpectreConsole/PwshSpectreConsole.psd1 b/PwshSpectreConsole/PwshSpectreConsole.psd1 index 61c7ffcd..0a9a4601 100644 --- a/PwshSpectreConsole/PwshSpectreConsole.psd1 +++ b/PwshSpectreConsole/PwshSpectreConsole.psd1 @@ -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 = @() @@ -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 = @() diff --git a/PwshSpectreConsole/formatting/Spectre.Console.AnsiConsole.format.ps1 b/PwshSpectreConsole/formatting/Spectre.Console.AnsiConsole.format.ps1 new file mode 100644 index 00000000..ca2a46ae --- /dev/null +++ b/PwshSpectreConsole/formatting/Spectre.Console.AnsiConsole.format.ps1 @@ -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 $_ + } +} \ No newline at end of file diff --git a/PwshSpectreConsole/private/Add-SpectreTreeNode.ps1 b/PwshSpectreConsole/private/Add-SpectreTreeNode.ps1 index 11d20696..4d042d81 100644 --- a/PwshSpectreConsole/private/Add-SpectreTreeNode.ps1 +++ b/PwshSpectreConsole/private/Add-SpectreTreeNode.ps1 @@ -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 + } + } } \ No newline at end of file diff --git a/PwshSpectreConsole/private/Convert-HashtableToRenderSafePSObject.ps1 b/PwshSpectreConsole/private/Convert-HashtableToRenderSafePSObject.ps1 new file mode 100644 index 00000000..47c95568 --- /dev/null +++ b/PwshSpectreConsole/private/Convert-HashtableToRenderSafePSObject.ps1 @@ -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 +} \ No newline at end of file diff --git a/PwshSpectreConsole/private/Write-AnsiConsole.ps1 b/PwshSpectreConsole/private/Write-AnsiConsole.ps1 index e8e5c78f..771fe913 100644 --- a/PwshSpectreConsole/private/Write-AnsiConsole.ps1 +++ b/PwshSpectreConsole/private/Write-AnsiConsole.ps1 @@ -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) } \ No newline at end of file diff --git a/PwshSpectreConsole/private/classes/PwshSpectreConsole.csproj b/PwshSpectreConsole/private/classes/PwshSpectreConsole.csproj index fab22433..7aa10385 100644 --- a/PwshSpectreConsole/private/classes/PwshSpectreConsole.csproj +++ b/PwshSpectreConsole/private/classes/PwshSpectreConsole.csproj @@ -5,7 +5,7 @@ - + diff --git a/PwshSpectreConsole/private/completions/Completers.psm1 b/PwshSpectreConsole/private/completions/Completers.psm1 index 099afe75..d32d3080 100644 --- a/PwshSpectreConsole/private/completions/Completers.psm1 +++ b/PwshSpectreConsole/private/completions/Completers.psm1 @@ -1,79 +1,108 @@ -using namespace Spectre.Console -using namespace System.Management.Automation - -class ValidateSpectreColor : ValidateArgumentsAttribute { - ValidateSpectreColor() : base() { } - [void]Validate([object] $Color, [EngineIntrinsics]$EngineIntrinsics) { - # Handle hex colors - if ($Color -match '^#[A-Fa-f0-9]{6}$') { - return - } - # Handle an explicitly defined spectre color object - if ($Color -is [Color]) { - return - } - $spectreColors = [Color] | Get-Member -Static -Type Properties | Select-Object -ExpandProperty Name - $result = $spectreColors -contains $Color - if ($result -eq $false) { - throw "'$Color' is not in the list of valid Spectre colors ['$($spectreColors -join ''', ''')']" - } - } -} - -class ArgumentCompletionsSpectreColors : ArgumentCompleterAttribute { - ArgumentCompletionsSpectreColors() : base({ - param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters) - $options = [Color] | Get-Member -Static -Type Properties | Select-Object -ExpandProperty Name - return $options | Where-Object { $_ -like "$wordToComplete*" } - }) { } -} - -class SpectreConsoleTableBorder : IValidateSetValuesGenerator { - [String[]] GetValidValues() { - $lookup = [TableBorder] | Get-Member -Static -MemberType Properties | Select-Object -ExpandProperty Name - return $lookup - } -} - -class SpectreConsoleBoxBorder : IValidateSetValuesGenerator { - [String[]] GetValidValues() { - $lookup = [BoxBorder] | Get-Member -Static -MemberType Properties | Select-Object -ExpandProperty Name - return $lookup - } -} - -class SpectreConsoleJustify : IValidateSetValuesGenerator { - [String[]] GetValidValues() { - $lookup = [Justify].GetEnumNames() - return $lookup - } -} - -class SpectreConsoleSpinner : IValidateSetValuesGenerator { - [String[]] GetValidValues() { - $lookup = [Spinner+Known] | Get-Member -Static -MemberType Properties | Select-Object -ExpandProperty Name - return $lookup - } -} - -class SpectreConsoleTreeGuide : IValidateSetValuesGenerator { - [String[]] GetValidValues() { - $lookup = [TreeGuide] | Get-Member -Static -MemberType Properties | Select-Object -ExpandProperty Name - 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]") - } -} +using namespace Spectre.Console +using namespace System.Management.Automation + +class ValidateSpectreColor : ValidateArgumentsAttribute { + + static[void]ValidateItem([object] $ItemColor) { + # Handle hex colors + if ($ItemColor -match '^#[A-Fa-f0-9]{6}$') { + return + } + # Handle an explicitly defined spectre color object + if ($ItemColor -is [Color]) { + return + } + $spectreColors = [Color] | Get-Member -Static -Type Properties | Select-Object -ExpandProperty Name + $result = $spectreColors -contains $ItemColor + if ($result -eq $false) { + throw "'$ItemColor' is not in the list of valid Spectre colors ['$($spectreColors -join ''', ''')']" + } + } + + ValidateSpectreColor() : base() { } + [void]Validate([object] $Color, [EngineIntrinsics]$EngineIntrinsics) { + [ValidateSpectreColor]::ValidateItem($Color) + } +} + +class ValidateSpectreColorTheme : ValidateArgumentsAttribute { + ValidateSpectreColorTheme() : base() { } + [void]Validate([object] $Colors, [EngineIntrinsics]$EngineIntrinsics) { + if ($Colors -isnot [hashtable]) { + throw "Color theme must be a hashtable of Spectre Console color names and values" + } + foreach ($color in $Colors.GetEnumerator()) { + [ValidateSpectreColor]::ValidateItem($color.Value) + } + } +} + +class ValidateSpectreTreeItem : ValidateArgumentsAttribute { + + static[void]ValidateItem([object] $TreeItem) { + # These objects are already renderable + if ($TreeItem -isnot [hashtable]) { + throw "Input for Spectre Tree must be a hashtable with 'Value' (and the optional 'Children') keys" + } + + if ($TreeItem.Keys -notcontains "Value") { + throw "Input for Spectre Tree must be a hashtable with 'Value' (and the optional 'Children') keys" + } + + if ($TreeItem.Keys -contains "Children") { + if ($TreeItem.Children -isnot [array]) { + throw "Children must be an array of tree items (hashtables with 'Value' and 'Children' keys)" + } + foreach ($child in $TreeItem.Children) { + [ValidateSpectreTreeItem]::ValidateItem($child) + } + } + } + + [void]Validate([object] $TreeItem, [EngineIntrinsics]$EngineIntrinsics) { + [ValidateSpectreTreeItem]::ValidateItem($TreeItem) + } +} + +class ArgumentCompletionsSpectreColors : ArgumentCompleterAttribute { + ArgumentCompletionsSpectreColors() : base({ + param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters) + $options = [Color] | Get-Member -Static -Type Properties | Select-Object -ExpandProperty Name + return $options | Where-Object { $_ -like "$wordToComplete*" } + }) { } +} + +class SpectreConsoleTableBorder : IValidateSetValuesGenerator { + [String[]] GetValidValues() { + $lookup = [TableBorder] | Get-Member -Static -MemberType Properties | Select-Object -ExpandProperty Name + return $lookup + } +} + +class SpectreConsoleBoxBorder : IValidateSetValuesGenerator { + [String[]] GetValidValues() { + $lookup = [BoxBorder] | Get-Member -Static -MemberType Properties | Select-Object -ExpandProperty Name + return $lookup + } +} + +class SpectreConsoleJustify : IValidateSetValuesGenerator { + [String[]] GetValidValues() { + $lookup = [Justify].GetEnumNames() + return $lookup + } +} + +class SpectreConsoleSpinner : IValidateSetValuesGenerator { + [String[]] GetValidValues() { + $lookup = [Spinner+Known] | Get-Member -Static -MemberType Properties | Select-Object -ExpandProperty Name + return $lookup + } +} + +class SpectreConsoleTreeGuide : IValidateSetValuesGenerator { + [String[]] GetValidValues() { + $lookup = [TreeGuide] | Get-Member -Static -MemberType Properties | Select-Object -ExpandProperty Name + return $lookup + } +} diff --git a/PwshSpectreConsole/private/completions/Transformers.psm1 b/PwshSpectreConsole/private/completions/Transformers.psm1 new file mode 100644 index 00000000..2e8ca9a0 --- /dev/null +++ b/PwshSpectreConsole/private/completions/Transformers.psm1 @@ -0,0 +1,90 @@ +using module "..\models\SpectreChartItem.psm1" +using namespace Spectre.Console +using namespace System.Management.Automation + +class ColorTransformationAttribute : ArgumentTransformationAttribute { + + static [object] TransformItem([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.GetType().FullName) '$InputData' to [Spectre.Console.Color]") + } + + [object] Transform([EngineIntrinsics]$engine, [object]$inputData) { + return [ColorTransformationAttribute]::TransformItem($inputData) + } +} + +class ColorThemeTransformationAttribute : ArgumentTransformationAttribute { + [object] Transform([EngineIntrinsics]$engine, [object]$inputData) { + if ($inputData -isnot [hashtable]) { + throw "Color theme must be a hashtable of Spectre Console color names and values" + } + $outputData = @{} + foreach ($color in $inputData.GetEnumerator()) { + $colorValue = [ColorTransformationAttribute]::TransformItem($color.Value) + if ($null -ne $colorValue) { + $outputData[$color.Key] = $colorValue + } else { + $spectreColors = [Color] | Get-Member -Static -Type Properties | Select-Object -ExpandProperty Name + throw "Invalid color value '$($color.Value)' for key '$($color.Key)' could not be mapped to one of the list of valid Spectre colors ['$($spectreColors -join ''', ''')']" + } + } + return $outputData + } +} + +class RenderableTransformationAttribute : ArgumentTransformationAttribute { + [object] Transform([EngineIntrinsics]$engine, [object]$inputData) { + # Converting data from a Format-* cmdlet to a Spectre Console object is not supported as it's already formatted for console output by the default host + if ($InputData.GetType().FullName -like "*Internal.Format*") { + throw "Cannot convert PowerShell Format data to be Spectre Console compatible. This object has likely already been formatted with a Format-* cmdlet." + } + + # These objects are already renderable + if ($InputData -is [Rendering.Renderable]) { + return $InputData + } + + # For others just dump them as either strings formatted with markup which are easy to identify by the closing tag [/] or as plain text + if ($InputData -like "*[/]*") { + return [Markup]::new($InputData) + } else { + return [Text]::new(($InputData | Out-String)) + } + } +} + +class ChartItemTransformationAttribute : ArgumentTransformationAttribute { + [object] Transform([EngineIntrinsics]$engine, [object]$inputData) { + + # These objects are already renderable + if ($InputData -is [SpectreChartItem]) { + return $InputData + } + + if ($inputData -is [hashtable]) { + if ($inputData.Keys -contains "Label" -and $inputData.Keys -contains "Value" -and $inputData.Keys -contains "Color") { + return [SpectreChartItem]::new($inputData.Label, $inputData.Value, $inputData.Color) + } + throw "Hashtable must contain 'Label', 'Value', and 'Color' keys to be converted to a [SpectreChartItem]" + } + + if ($inputData -is [PSCustomObject]) { + if ($inputData.PSObject.Properties.Name -contains "Label" -and $inputData.PSObject.Properties.Name -contains "Value" -and $inputData.PSObject.Properties.Name -contains "Color") { + return [SpectreChartItem]::new($inputData.Label, $inputData.Value, $inputData.Color) + } + throw "PSCustomObject must contain 'Label', 'Value', and 'Color' properties to be converted to a [SpectreChartItem]" + } + + throw "Cannot convert $($inputData.GetType().FullName) to [SpectreChartItem]. Expected a hashtable or PSCustomObject with 'Label', 'Value', and 'Color' properties." + } +} diff --git a/PwshSpectreConsole/public/config/Set-SpectreColors.ps1 b/PwshSpectreConsole/public/config/Set-SpectreColors.ps1 index 1b1d27d4..531a00c1 100644 --- a/PwshSpectreConsole/public/config/Set-SpectreColors.ps1 +++ b/PwshSpectreConsole/public/config/Set-SpectreColors.ps1 @@ -1,50 +1,51 @@ -using module "..\..\private\completions\Completers.psm1" -using namespace Spectre.Console - -function Set-SpectreColors { - <# - .SYNOPSIS - Sets the accent color and default value color for Spectre Console. - - .DESCRIPTION - This function sets the accent color and default value color for Spectre Console. The accent color is used for highlighting important information, while the default value color is used for displaying default values. - - .PARAMETER AccentColor - The accent color to set. Must be a valid Spectre Console color name. Defaults to "Blue". - - .PARAMETER DefaultValueColor - The default value color to set. Must be a valid Spectre Console color name. Defaults to "Grey". - - .PARAMETER DefaultTableHeaderColor - The default table header color to set. Must be a valid Spectre Console color name. Defaults to "Default" which will be the standard console foreground color. - - .PARAMETER DefaultTableTextColor - The default table text color to set. Must be a valid Spectre Console color name. Defaults to "Default" which will be the standard console foreground color. - - .EXAMPLE - Write-SpectreRule "This is a default rule" - Set-SpectreColors -AccentColor "Turquoise2" - Write-SpectreRule "This is a Turquoise2 rule" - Write-SpectreRule "This is a rule with a specified color" -Color "Yellow" - - #> - [Reflection.AssemblyMetadata("title", "Set-SpectreColors")] - param ( - [ColorTransformationAttribute()] - [ArgumentCompletionsSpectreColors()] - [Color] $AccentColor = "Blue", - [ColorTransformationAttribute()] - [ArgumentCompletionsSpectreColors()] - [Color] $DefaultValueColor = "Grey", - [ColorTransformationAttribute()] - [ArgumentCompletionsSpectreColors()] - [Color] $DefaultTableHeaderColor = [Color]::Default, - [ColorTransformationAttribute()] - [ArgumentCompletionsSpectreColors()] - [Color] $DefaultTableTextColor = [Color]::Default - ) - $script:AccentColor = $AccentColor - $script:DefaultValueColor = $DefaultValueColor - $script:DefaultTableHeaderColor = $DefaultTableHeaderColor - $script:DefaultTableTextColor = $DefaultTableTextColor +using module "..\..\private\completions\Completers.psm1" +using module "..\..\private\completions\Transformers.psm1" +using namespace Spectre.Console + +function Set-SpectreColors { + <# + .SYNOPSIS + Sets the accent color and default value color for Spectre Console. + + .DESCRIPTION + This function sets the accent color and default value color for Spectre Console. The accent color is used for highlighting important information, while the default value color is used for displaying default values. + + .PARAMETER AccentColor + The accent color to set. Must be a valid Spectre Console color name. Defaults to "Blue". + + .PARAMETER DefaultValueColor + The default value color to set. Must be a valid Spectre Console color name. Defaults to "Grey". + + .PARAMETER DefaultTableHeaderColor + The default table header color to set. Must be a valid Spectre Console color name. Defaults to "Default" which will be the standard console foreground color. + + .PARAMETER DefaultTableTextColor + The default table text color to set. Must be a valid Spectre Console color name. Defaults to "Default" which will be the standard console foreground color. + + .EXAMPLE + Write-SpectreRule "This is a default rule" + Set-SpectreColors -AccentColor "Turquoise2" + Write-SpectreRule "This is a Turquoise2 rule" + Write-SpectreRule "This is a rule with a specified color" -Color "Yellow" + + #> + [Reflection.AssemblyMetadata("title", "Set-SpectreColors")] + param ( + [ColorTransformationAttribute()] + [ArgumentCompletionsSpectreColors()] + [Color] $AccentColor = "Blue", + [ColorTransformationAttribute()] + [ArgumentCompletionsSpectreColors()] + [Color] $DefaultValueColor = "Grey", + [ColorTransformationAttribute()] + [ArgumentCompletionsSpectreColors()] + [Color] $DefaultTableHeaderColor = [Color]::Default, + [ColorTransformationAttribute()] + [ArgumentCompletionsSpectreColors()] + [Color] $DefaultTableTextColor = [Color]::Default + ) + $script:AccentColor = $AccentColor + $script:DefaultValueColor = $DefaultValueColor + $script:DefaultTableHeaderColor = $DefaultTableHeaderColor + $script:DefaultTableTextColor = $DefaultTableTextColor } \ No newline at end of file diff --git a/PwshSpectreConsole/public/demo/Start-SpectreDemo.ps1 b/PwshSpectreConsole/public/demo/Start-SpectreDemo.ps1 index 61a3da84..fca0880f 100644 --- a/PwshSpectreConsole/public/demo/Start-SpectreDemo.ps1 +++ b/PwshSpectreConsole/public/demo/Start-SpectreDemo.ps1 @@ -1,332 +1,332 @@ -using namespace Spectre.Console - -function Write-SpectreExample { - param ( - [Parameter(ValueFromPipeline)] - [string] $Codeblock, - [string] $Title, - [string] $Description, - [switch] $HideHeader, - [switch] $NoNewline - ) - if ($host.UI.RawUI.WindowSize.Width -lt 120) { - Write-SpectreFigletText "Pwsh + Spectre!" - } else { - Write-SpectreFigletText "Welcome to PwshSpectreConsole!" - } - Write-Host "" - - Write-SpectreRule $Title -Color ([Color]::SteelBlue1) - Write-SpectreHost "`n$Description" - if (!$HideHeader) { - Write-CodeblockHeader - } - $Codeblock | Write-Codeblock -SyntaxHighlight -ShowLineNumbers - if (!$NoNewline) { - Write-Host "" - } -} - -function Start-SpectreDemo { - <# - .SYNOPSIS - Runs a demo of the PwshSpectreConsole module. - - .DESCRIPTION - This function runs a demo of the PwshSpectreConsole module, showcasing some of its features. - It displays various examples of Spectre.Console functionality wrapped in PowerShell functions, such as text entry, select lists, multi-select lists, and panels. - ![Spectre demo animation](/demo.gif) - - .EXAMPLE - Start-SpectreDemo - #> - [Reflection.AssemblyMetadata("title", "Start-SpectreDemo")] - param() - - Clear-Host - - if ($host.UI.RawUI.WindowSize.Width -lt 120) { - Write-SpectreFigletText "Pwsh + Spectre!" - } else { - Write-SpectreFigletText "Welcome to PwshSpectreConsole!" - } - Write-Host "" - - Write-SpectreRule "PwshSpectreConsole Intro" -Color ([Color]::SteelBlue1) - Write-SpectreHost "`nPwshSpectreConsole is an opinionated wrapper for the awesome Spectre.Console library. It's opinionated in that I have not just exposed the internals of Spectre Console to PowerShell but have wrapped them in a way that makes them work better in the PowerShell ecosystem (in my opinion 😜)." - Write-SpectreHost "`nSpectre Console is mostly an async library and it leans heavily on types and extension methods in C# which are very verbose to work with in PowerShell so this module hides away some of the complexity." - Write-SpectreHost "`nThe module doesn't expose the full feature set of Spectre.Console because the scope of the library is huge and I've focused on the features that I use to enhance my scripts." - Write-Host "" - if (![AnsiConsole]::Console.Profile.Capabilities.Unicode) { - Write-Warning "To enable all features of Spectre.Console you need to enable Unicode support in your PowerShell profile by adding the following to your profile at $PROFILE. See https://spectreconsole.net/best-practices for more info.`n`n`$OutputEncoding = [console]::InputEncoding = [console]::OutputEncoding = New-Object System.Text.UTF8Encoding`n" - } - - Read-SpectrePause -NoNewline - - Clear-Host - - $example = @' -$name = Read-SpectreText "What's your [blue]name[/]?" -AllowEmpty -'@ - $example | Write-SpectreExample -Title "Text Entry" -Description "Text entry is essential for user input and interaction in a terminal application. It allows users to enter commands, input data, or provide search queries, giving them the ability to interact with the application and perform tasks. The built-in PowerShell Read-Host has some additional functionality like auto-complete and history that the Spectre text entry doesn't have so Read-Host is usually a better option for text entry in your scripts." - $example | Invoke-Expression - Clear-Host - - $example = @' -$answer = Read-SpectreConfirm -Prompt "Do you like cute animals?" -DefaultAnswer "y" -'@ - $example | Write-SpectreExample -Title "Confirmation" -Description "Confirmation prompts are used to confirm an action or decision before it is executed. They help prevent users from making mistakes or taking actions they did not intend to, reducing the chances of errors and improving the overall user experience." - $example | Invoke-Expression - Clear-Host - - $example = @' -$choices = @("Sushi", "Tacos", "Pad Thai", "Lobster", "Falafel", "Chicken Parmesan", "Ramen", "Fish and Chips", "Biryani", "Croissants", "Enchiladas", "Shepherd's Pie", "Gyoza", "Fajitas", "Samosas", "Bruschetta", "Paella", "Hamburger", "Poutine", "Ceviche") - -$food = Read-SpectreSelection ` - -Title "What's your favourite [blue]food[/]?" ` - -Choices $choices -'@ - $example | Write-SpectreExample -Title "Select Lists" -Description "Select lists are helpful for presenting multiple options to the user in an organized manner. They enable users to choose one option from a list, simplifying decision-making and input tasks and reducing the chances of errors. The list will also paginate if there are too many options to show all at once." - $example | Invoke-Expression - Clear-Host - - $example = @' -$choices = @( - @{ Name = "RGB"; Choices = @("red", "green", "blue") }, - @{ Name = "CMYK"; Choices = @("cyan", "magenta", "yellow", "black") } -) - -$colors = Read-SpectreMultiSelectionGrouped ` - -Title "What's your favourite [blue]color[/]?" ` - -Choices $choices ` - -AllowEmpty -'@ - $example | Write-SpectreExample -Title "Multi-Select Lists" -Description "Multi-select lists allow users to choose multiple options from a list. This feature is useful when users need to perform operations on multiple items at once, such as selecting multiple files to delete or copy. The multi-select lists also allow categorizing items so you can select the whole category at once." - $example | Invoke-Expression - Clear-Host - - $example = @' -$message = "Hi $name, nice to meet you :waving_hand:`n" -$message += "Your favourite food is $food :fork_and_knife:`n" -$message += "And your favourite colors are:`n" -if($colors) { - foreach($color in $colors) { - $message += " - [$color]$color[/]`n" - } -} else { - $message += "Nothing, you didn't select any colors :crying_face:" -} -$message += "Nice! :rainbow:" - -$message | Format-SpectrePanel -Title "Output" -'@ - $example | Write-SpectreExample -Title "Panels" -Description "Panels are used to separate and organize different sections or groups of information within a terminal application. They help keep the interface clean and structured, making it easier for users to navigate and understand the application." - Invoke-Expression $example - - Read-SpectrePause - Clear-Host - - $example = @' -Get-Process | Select-Object -First 10 -Property Id, Name, Handles | Format-SpectreTable -'@ - $example | Write-SpectreExample -Title "Tables" -Description "Tables are an effective way to display structured data in a terminal application. They provide a clear and organized representation of data, making it easier for users to understand, compare, and analyze the information. The tables in Spectre Console are not as feature rich as the built-in PowerShell Format-Table but they can have more visual impact." - Invoke-Expression $example - - Read-SpectrePause - Clear-Host - - $example = @' -@{ - Label = "Root" - Children = @( - @{ - Label = "First Child" - Children = @( - @{ Label = "With"; Children = @() }, - @{ Label = "Loads"; Children = @() }, - @{ Label = "More"; Children = @() }, - @{ Label = "Nested"; Children = @( @{ Label = "Children"; Children = @() } ) } - ) - }, - @{ Label = "Second Child"; Children = @() } - ) -} | Format-SpectreTree -'@ - $example | Write-SpectreExample -Title "Tree Diagrams" -Description "Tree diagrams help visualize hierarchical relationships between different elements in a dataset. They are particularly useful in applications dealing with file systems, organizational structures, or nested data, providing an intuitive representation of the structure." - Invoke-Expression $example - - Read-SpectrePause - Clear-Host - - $example = @' -$( - @{ - Label = "Apple" - Value = 12 - Color = [Color]::Green - }, - @{ - Label = "Orange" - Value = 54 - Color = [Color]::Orange1 - }, - @{ - Label = "Strawberry" - Value = 51 - Color = [Color]::Red - }, - @{ - Label = "Banana" - Value = 33 - Color = [Color]::Yellow - } -) | Format-SpectreBarChart -'@ - $example | Write-SpectreExample -Title "Bar Charts" -Description "Bar charts are a powerful way to visualize data comparisons in a terminal application. They can represent various data types, such as categorical or numerical data, making it easier for users to identify trends, patterns, and differences between data points." - Invoke-Expression $example - - Read-SpectrePause - Clear-Host - - $example = @' -$( - @{ - Label = "Apple" - Value = 12 - Color = [Color]::Green - }, - @{ - Label = "Strawberry" - Value = 15 - Color = [Color]::Red - }, - @{ - Label = "Orange" - Value = 54 - Color = [Color]::Orange1 - }, - @{ - Label = "Plum" - Value = 75 - Color = [Color]::Fuchsia - } -) | Format-SpectreBreakdownChart -'@ - $example | Write-SpectreExample -Title "Breakdown Charts" -Description "Like a pie chart but horizontal, breakdown charts can be used to show the proportions of a total that components make up." - Invoke-Expression $example - Read-SpectrePause - Clear-Host - - $example = @' -Get-Module PwshSpectreConsole | Select-Object PrivateData | Format-SpectreJson -Expand -'@ - $example | Write-SpectreExample -Title "Json Data" -Description "Spectre Console can format JSON with syntax highlighting thanks to https://github.com/trackd" - Invoke-Expression $example - Read-SpectrePause - Clear-Host - - $example = @' -Invoke-SpectreCommandWithStatus -Spinner "Dots2" -Title "Showing a spinner..." -ScriptBlock { - # Write updates to the host using Write-SpectreHost - Start-Sleep -Seconds 1 - Write-SpectreHost "`n[grey]LOG:[/] Doing some work " - Start-Sleep -Seconds 1 - Write-SpectreHost "`n[grey]LOG:[/] Doing some more work " - Start-Sleep -Seconds 1 - Write-SpectreHost "`n[grey]LOG:[/] Done " - Start-Sleep -Seconds 1 -} -'@ - $example | Write-SpectreExample -Title "Progress Spinners" -Description "Progress spinners provide visual feedback to users when an operation or task is in progress. They help indicate that the application is working on a request, preventing users from becoming frustrated or assuming the application has stalled." - Invoke-Expression $example - - Read-SpectrePause - Clear-Host - - $example = @' -Invoke-SpectreCommandWithProgress -ScriptBlock { - param ( - $ctx - ) - $task1 = $ctx.AddTask("A 4-stage process") - Start-Sleep -Seconds 1 - $task1.Increment(25) - Start-Sleep -Seconds 1 - $task1.Increment(25) - Start-Sleep -Seconds 1 - $task1.Increment(25) - Start-Sleep -Seconds 1 - $task1.Increment(25) - Start-Sleep -Seconds 1 -} -'@ - $example | Write-SpectreExample -NoNewline -Title "Progress Bars" -Description "Progress bars give users a visual indication of the progress of a specific task or operation. They show the percentage of completion, helping users understand how much work remains and providing a sense of time estimation for the task." - Invoke-Expression $example - - Read-SpectrePause -NoNewline - Clear-Host - - $example = @' -Invoke-SpectreCommandWithProgress -ScriptBlock { - param ( $ctx ) - - $jobs = @() - $jobs += Add-SpectreJob -Context $ctx -JobName "Drawing a picture" -Job ( - Start-Job { - $progress = 0 - while($progress -lt 100) { - $progress += 1.5 - Write-Progress -Activity "Processing" -PercentComplete $progress - Start-Sleep -Milliseconds 50 - } - } - ) - $jobs += Add-SpectreJob -Context $ctx -JobName "Driving a car" -Job ( - Start-Job { - $progress = 0 - while($progress -lt 100) { - $progress += 0.9 - Write-Progress -Activity "Processing" -PercentComplete $progress - Start-Sleep -Milliseconds 50 - } - } - ) - - Wait-SpectreJobs -Context $ctx -Jobs $jobs -} -'@ - $example | Write-SpectreExample -NoNewline -Title "Parallel Progress Bars" -Description "Parallel progress bars are used to display the progress of multiple tasks or operations simultaneously. They are useful in applications that perform several tasks concurrently, allowing users to monitor the status of each task individually." - Invoke-Expression $example - - Read-SpectrePause -NoNewline - Clear-Host - - $example = @' -Invoke-SpectreCommandWithProgress -ScriptBlock { - param ( - $ctx - ) - $task1 = $ctx.AddTask("Doing something") - $task1.IsIndeterminate = $true - Start-Sleep -Seconds 10 - $task1.IsIndeterminate = $false - $task1.Value = 100 - $task1.StopTask() -} -'@ - $example | Write-SpectreExample -NoNewline -Title "Indeterminate Progress Bars" -Description "Indeterminate progress bars are used when the duration of a task or operation is unknown or cannot be accurately estimated. They provide a visual indication that work is being done, even if the exact progress cannot be determined, reassuring users that the application is still functioning. You could also use spinners to portray this information but sometimes you want to be consistent if you're already using progress bars for other tasks." - Invoke-Expression $example - - Read-SpectrePause -NoNewline - Clear-Host - - $example = @" -Get-SpectreImageExperimental "$PSScriptRoot\..\..\private\images\harveyspecter.gif" -LoopCount 2 -Write-SpectreHost "I'm Harvey Specter. Are you after a Specter consult or a Spectre.Console?" -"@ - $example | Write-SpectreExample -Title "View Images" -Description "Images can be rendered in the terminal, given a path to an image Spectre Console will downsample the image to a resolution that will fit within the terminal width or you can choose your own width setting." - Invoke-Expression $example - - Read-SpectrePause - Write-Host "" +using namespace Spectre.Console + +function Write-SpectreExample { + param ( + [Parameter(ValueFromPipeline)] + [string] $Codeblock, + [string] $Title, + [string] $Description, + [switch] $HideHeader, + [switch] $NoNewline + ) + if ($host.UI.RawUI.WindowSize.Width -lt 120) { + Write-SpectreFigletText "Pwsh + Spectre!" + } else { + Write-SpectreFigletText "Welcome to PwshSpectreConsole!" + } + Write-Host "" + + Write-SpectreRule $Title -Color ([Color]::SteelBlue1) + Write-SpectreHost "`n$Description" + if (!$HideHeader) { + Write-CodeblockHeader + } + $Codeblock | Write-Codeblock -SyntaxHighlight -ShowLineNumbers + if (!$NoNewline) { + Write-Host "" + } +} + +function Start-SpectreDemo { + <# + .SYNOPSIS + Runs a demo of the PwshSpectreConsole module. + + .DESCRIPTION + This function runs a demo of the PwshSpectreConsole module, showcasing some of its features. + It displays various examples of Spectre.Console functionality wrapped in PowerShell functions, such as text entry, select lists, multi-select lists, and panels. + ![Spectre demo animation](/demo.gif) + + .EXAMPLE + Start-SpectreDemo + #> + [Reflection.AssemblyMetadata("title", "Start-SpectreDemo")] + param() + + Clear-Host + + if ($host.UI.RawUI.WindowSize.Width -lt 120) { + Write-SpectreFigletText "Pwsh + Spectre!" + } else { + Write-SpectreFigletText "Welcome to PwshSpectreConsole!" + } + Write-Host "" + + Write-SpectreRule "PwshSpectreConsole Intro" -Color ([Color]::SteelBlue1) + Write-SpectreHost "`nPwshSpectreConsole is an opinionated wrapper for the awesome Spectre.Console library. It's opinionated in that I have not just exposed the internals of Spectre Console to PowerShell but have wrapped them in a way that makes them work better in the PowerShell ecosystem (in my opinion 😜)." + Write-SpectreHost "`nSpectre Console is mostly an async library and it leans heavily on types and extension methods in C# which are very verbose to work with in PowerShell so this module hides away some of the complexity." + Write-SpectreHost "`nThe module doesn't expose the full feature set of Spectre.Console because the scope of the library is huge and I've focused on the features that I use to enhance my scripts." + Write-Host "" + if (![AnsiConsole]::Console.Profile.Capabilities.Unicode) { + Write-Warning "To enable all features of Spectre.Console you need to enable Unicode support in your PowerShell profile by adding the following to your profile at $PROFILE. See https://spectreconsole.net/best-practices for more info.`n`n`$OutputEncoding = [console]::InputEncoding = [console]::OutputEncoding = New-Object System.Text.UTF8Encoding`n" + } + + Read-SpectrePause -NoNewline + + Clear-Host + + $example = @' +$name = Read-SpectreText "What's your [blue]name[/]?" -AllowEmpty +'@ + $example | Write-SpectreExample -Title "Text Entry" -Description "Text entry is essential for user input and interaction in a terminal application. It allows users to enter commands, input data, or provide search queries, giving them the ability to interact with the application and perform tasks. The built-in PowerShell Read-Host has some additional functionality like auto-complete and history that the Spectre text entry doesn't have so Read-Host is usually a better option for text entry in your scripts." + $example | Invoke-Expression + Clear-Host + + $example = @' +$answer = Read-SpectreConfirm -Prompt "Do you like cute animals?" -DefaultAnswer "y" +'@ + $example | Write-SpectreExample -Title "Confirmation" -Description "Confirmation prompts are used to confirm an action or decision before it is executed. They help prevent users from making mistakes or taking actions they did not intend to, reducing the chances of errors and improving the overall user experience." + $example | Invoke-Expression + Clear-Host + + $example = @' +$choices = @("Sushi", "Tacos", "Pad Thai", "Lobster", "Falafel", "Chicken Parmesan", "Ramen", "Fish and Chips", "Biryani", "Croissants", "Enchiladas", "Shepherd's Pie", "Gyoza", "Fajitas", "Samosas", "Bruschetta", "Paella", "Hamburger", "Poutine", "Ceviche") + +$food = Read-SpectreSelection ` + -Title "What's your favourite [blue]food[/]?" ` + -Choices $choices +'@ + $example | Write-SpectreExample -Title "Select Lists" -Description "Select lists are helpful for presenting multiple options to the user in an organized manner. They enable users to choose one option from a list, simplifying decision-making and input tasks and reducing the chances of errors. The list will also paginate if there are too many options to show all at once." + $example | Invoke-Expression + Clear-Host + + $example = @' +$choices = @( + @{ Name = "RGB"; Choices = @("red", "green", "blue") }, + @{ Name = "CMYK"; Choices = @("cyan", "magenta", "yellow", "black") } +) + +$colors = Read-SpectreMultiSelectionGrouped ` + -Title "What's your favourite [blue]color[/]?" ` + -Choices $choices ` + -AllowEmpty +'@ + $example | Write-SpectreExample -Title "Multi-Select Lists" -Description "Multi-select lists allow users to choose multiple options from a list. This feature is useful when users need to perform operations on multiple items at once, such as selecting multiple files to delete or copy. The multi-select lists also allow categorizing items so you can select the whole category at once." + $example | Invoke-Expression + Clear-Host + + $example = @' +$message = "Hi $name, nice to meet you :waving_hand:`n" +$message += "Your favourite food is $food :fork_and_knife:`n" +$message += "And your favourite colors are:`n" +if($colors) { + foreach($color in $colors) { + $message += " - [$color]$color[/]`n" + } +} else { + $message += "Nothing, you didn't select any colors :crying_face:" +} +$message += "Nice! :rainbow:" + +$message | Format-SpectrePanel -Title "Output" +'@ + $example | Write-SpectreExample -Title "Panels" -Description "Panels are used to separate and organize different sections or groups of information within a terminal application. They help keep the interface clean and structured, making it easier for users to navigate and understand the application." + Invoke-Expression $example + + Read-SpectrePause + Clear-Host + + $example = @' +Get-Process | Select-Object -First 10 -Property Id, Name, Handles | Format-SpectreTable +'@ + $example | Write-SpectreExample -Title "Tables" -Description "Tables are an effective way to display structured data in a terminal application. They provide a clear and organized representation of data, making it easier for users to understand, compare, and analyze the information. The tables in Spectre Console are not as feature rich as the built-in PowerShell Format-Table but they can have more visual impact." + Invoke-Expression $example + + Read-SpectrePause + Clear-Host + + $example = @' +@{ + Value = "Root" + Children = @( + @{ + Value = "First Child" + Children = @( + @{ Value = "With" }, + @{ Value = "Loads" }, + @{ Value = "More" }, + @{ Value = "Nested"; Children = @( @{ Value = "Children" } ) } + ) + }, + @{ Value = "Second Child" } + ) +} | Format-SpectreTree +'@ + $example | Write-SpectreExample -Title "Tree Diagrams" -Description "Tree diagrams help visualize hierarchical relationships between different elements in a dataset. They are particularly useful in applications dealing with file systems, organizational structures, or nested data, providing an intuitive representation of the structure." + Invoke-Expression $example + + Read-SpectrePause + Clear-Host + + $example = @' +$( + @{ + Label = "Apple" + Value = 12 + Color = [Color]::Green + }, + @{ + Label = "Orange" + Value = 54 + Color = [Color]::Orange1 + }, + @{ + Label = "Strawberry" + Value = 51 + Color = [Color]::Red + }, + @{ + Label = "Banana" + Value = 33 + Color = [Color]::Yellow + } +) | Format-SpectreBarChart +'@ + $example | Write-SpectreExample -Title "Bar Charts" -Description "Bar charts are a powerful way to visualize data comparisons in a terminal application. They can represent various data types, such as categorical or numerical data, making it easier for users to identify trends, patterns, and differences between data points." + Invoke-Expression $example + + Read-SpectrePause + Clear-Host + + $example = @' +$( + @{ + Label = "Apple" + Value = 12 + Color = [Color]::Green + }, + @{ + Label = "Strawberry" + Value = 15 + Color = [Color]::Red + }, + @{ + Label = "Orange" + Value = 54 + Color = [Color]::Orange1 + }, + @{ + Label = "Plum" + Value = 75 + Color = [Color]::Fuchsia + } +) | Format-SpectreBreakdownChart +'@ + $example | Write-SpectreExample -Title "Breakdown Charts" -Description "Like a pie chart but horizontal, breakdown charts can be used to show the proportions of a total that components make up." + Invoke-Expression $example + Read-SpectrePause + Clear-Host + + $example = @' +Get-Module PwshSpectreConsole | Select-Object PrivateData | Format-SpectreJson -Expand +'@ + $example | Write-SpectreExample -Title "Json Data" -Description "Spectre Console can format JSON with syntax highlighting thanks to https://github.com/trackd" + Invoke-Expression $example + Read-SpectrePause + Clear-Host + + $example = @' +Invoke-SpectreCommandWithStatus -Spinner "Dots2" -Title "Showing a spinner..." -ScriptBlock { + # Write updates to the host using Write-SpectreHost + Start-Sleep -Seconds 1 + Write-SpectreHost "`n[grey]LOG:[/] Doing some work " + Start-Sleep -Seconds 1 + Write-SpectreHost "`n[grey]LOG:[/] Doing some more work " + Start-Sleep -Seconds 1 + Write-SpectreHost "`n[grey]LOG:[/] Done " + Start-Sleep -Seconds 1 +} +'@ + $example | Write-SpectreExample -Title "Progress Spinners" -Description "Progress spinners provide visual feedback to users when an operation or task is in progress. They help indicate that the application is working on a request, preventing users from becoming frustrated or assuming the application has stalled." + Invoke-Expression $example + + Read-SpectrePause + Clear-Host + + $example = @' +Invoke-SpectreCommandWithProgress -ScriptBlock { + param ( + $ctx + ) + $task1 = $ctx.AddTask("A 4-stage process") + Start-Sleep -Seconds 1 + $task1.Increment(25) + Start-Sleep -Seconds 1 + $task1.Increment(25) + Start-Sleep -Seconds 1 + $task1.Increment(25) + Start-Sleep -Seconds 1 + $task1.Increment(25) + Start-Sleep -Seconds 1 +} +'@ + $example | Write-SpectreExample -NoNewline -Title "Progress Bars" -Description "Progress bars give users a visual indication of the progress of a specific task or operation. They show the percentage of completion, helping users understand how much work remains and providing a sense of time estimation for the task." + Invoke-Expression $example + + Read-SpectrePause -NoNewline + Clear-Host + + $example = @' +Invoke-SpectreCommandWithProgress -ScriptBlock { + param ( $ctx ) + + $jobs = @() + $jobs += Add-SpectreJob -Context $ctx -JobName "Drawing a picture" -Job ( + Start-Job { + $progress = 0 + while($progress -lt 100) { + $progress += 1.5 + Write-Progress -Activity "Processing" -PercentComplete $progress + Start-Sleep -Milliseconds 50 + } + } + ) + $jobs += Add-SpectreJob -Context $ctx -JobName "Driving a car" -Job ( + Start-Job { + $progress = 0 + while($progress -lt 100) { + $progress += 0.9 + Write-Progress -Activity "Processing" -PercentComplete $progress + Start-Sleep -Milliseconds 50 + } + } + ) + + Wait-SpectreJobs -Context $ctx -Jobs $jobs +} +'@ + $example | Write-SpectreExample -NoNewline -Title "Parallel Progress Bars" -Description "Parallel progress bars are used to display the progress of multiple tasks or operations simultaneously. They are useful in applications that perform several tasks concurrently, allowing users to monitor the status of each task individually." + Invoke-Expression $example + + Read-SpectrePause -NoNewline + Clear-Host + + $example = @' +Invoke-SpectreCommandWithProgress -ScriptBlock { + param ( + $ctx + ) + $task1 = $ctx.AddTask("Doing something") + $task1.IsIndeterminate = $true + Start-Sleep -Seconds 10 + $task1.IsIndeterminate = $false + $task1.Value = 100 + $task1.StopTask() +} +'@ + $example | Write-SpectreExample -NoNewline -Title "Indeterminate Progress Bars" -Description "Indeterminate progress bars are used when the duration of a task or operation is unknown or cannot be accurately estimated. They provide a visual indication that work is being done, even if the exact progress cannot be determined, reassuring users that the application is still functioning. You could also use spinners to portray this information but sometimes you want to be consistent if you're already using progress bars for other tasks." + Invoke-Expression $example + + Read-SpectrePause -NoNewline + Clear-Host + + $example = @" +Get-SpectreImageExperimental "$PSScriptRoot\..\..\private\images\harveyspecter.gif" -LoopCount 2 +Write-SpectreHost "I'm Harvey Specter. Are you after a Specter consult or a Spectre.Console?" +"@ + $example | Write-SpectreExample -Title "View Images" -Description "Images can be rendered in the terminal, given a path to an image Spectre Console will downsample the image to a resolution that will fit within the terminal width or you can choose your own width setting." + Invoke-Expression $example + + Read-SpectrePause + Write-Host "" } \ No newline at end of file diff --git a/PwshSpectreConsole/public/formatting/Format-SpectreBarChart.ps1 b/PwshSpectreConsole/public/formatting/Format-SpectreBarChart.ps1 index 7259ebcb..7fa97dbe 100644 --- a/PwshSpectreConsole/public/formatting/Format-SpectreBarChart.ps1 +++ b/PwshSpectreConsole/public/formatting/Format-SpectreBarChart.ps1 @@ -1,65 +1,68 @@ -using module "..\..\private\completions\Completers.psm1" -using namespace Spectre.Console - -function Format-SpectreBarChart { - <# - .SYNOPSIS - Formats and displays a bar chart using the Spectre Console module. - ![Example bar chart](/barchart.png) - - .DESCRIPTION - This function takes an array of data and displays it as a bar chart using the Spectre Console module. The chart can be customized with a title and width. - - .PARAMETER Data - An array of objects containing the data to be displayed in the chart. Each object should have a Label, Value, and Color property. - - .PARAMETER Title - The title to be displayed above the chart. - - .PARAMETER Width - The width of the chart in characters. - - .PARAMETER HideValues - Hides the values from being displayed on the chart. - - .EXAMPLE - $data = @() - - $data += New-SpectreChartItem -Label "Apples" -Value 10 -Color "Green" - $data += New-SpectreChartItem -Label "Oranges" -Value 5 -Color "DarkOrange" - $data += New-SpectreChartItem -Label "Bananas" -Value 2.2 -Color "#FFFF00" - - Format-SpectreBarChart -Data $data -Title "Fruit Sales" -Width 50 - #> - [Reflection.AssemblyMetadata("title", "Format-SpectreBarChart")] - param ( - [Parameter(ValueFromPipeline, Mandatory)] - [array] $Data, - [String] $Title, - [ValidateScript({ $_ -gt 0 -and $_ -le (Get-HostWidth) }, ErrorMessage = "Value '{0}' is invalid. Cannot be negative or exceed console width.")] - [int] $Width = (Get-HostWidth), - [switch] $HideValues - ) - begin { - $barChart = [BarChart]::new() - if ($Title) { - $barChart.Label = $Title - } - if ($HideValues) { - $barChart.ShowValues = $false - } - $barChart.Width = $Width - } - process { - if ($Data -is [array]) { - foreach ($dataItem in $Data) { - $barChart = [BarChartExtensions]::AddItem($barChart, $dataItem.Label, $dataItem.Value, ($dataItem.Color | Convert-ToSpectreColor)) - } - } else { - $barChart = [BarChartExtensions]::AddItem($barChart, $Data.Label, $Data.Value, ($Data.Color | Convert-ToSpectreColor)) - } - } - end { - Write-AnsiConsole $barChart - } -} +using module "..\..\private\completions\Completers.psm1" +using module "..\..\private\completions\Transformers.psm1" +using namespace Spectre.Console + +function Format-SpectreBarChart { + <# + .SYNOPSIS + Formats and displays a bar chart using the Spectre Console module. + ![Example bar chart](/barchart.png) + + .DESCRIPTION + This function takes an array of data and displays it as a bar chart using the Spectre Console module. The chart can be customized with a title and width. + + .PARAMETER Data + An array of objects containing the data to be displayed in the chart. Each object should have a Label, Value, and Color property. + + .PARAMETER Label + The title to be displayed above the chart. + + .PARAMETER Width + The width of the chart in characters. + + .PARAMETER HideValues + Hides the values from being displayed on the chart. + + .EXAMPLE + $data = @() + + $data += New-SpectreChartItem -Label "Apples" -Value 10 -Color "Green" + $data += New-SpectreChartItem -Label "Oranges" -Value 5 -Color "DarkOrange" + $data += New-SpectreChartItem -Label "Bananas" -Value 2.2 -Color "#FFFF00" + + Format-SpectreBarChart -Data $data -Label "Fruit Sales" -Width 50 + #> + [Reflection.AssemblyMetadata("title", "Format-SpectreBarChart")] + param ( + [Parameter(ValueFromPipeline, Mandatory)] + [ChartItemTransformationAttribute()] + [array] $Data, + [Alias("Title")] + [String] $Label, + [ValidateScript({ $_ -gt 0 -and $_ -le (Get-HostWidth) }, ErrorMessage = "Value '{0}' is invalid. Cannot be negative or exceed console width.")] + [int] $Width = (Get-HostWidth), + [switch] $HideValues + ) + begin { + $barChart = [BarChart]::new() + if ($Label) { + $barChart.Label = $Label + } + if ($HideValues) { + $barChart.ShowValues = $false + } + $barChart.Width = $Width + } + process { + if ($Data -is [array]) { + foreach ($dataItem in $Data) { + $barChart = [BarChartExtensions]::AddItem($barChart, $dataItem.Label, $dataItem.Value, ($dataItem.Color | Convert-ToSpectreColor)) + } + } else { + $barChart = [BarChartExtensions]::AddItem($barChart, $Data.Label, $Data.Value, ($Data.Color | Convert-ToSpectreColor)) + } + } + end { + return $barChart + } +} diff --git a/PwshSpectreConsole/public/formatting/Format-SpectreBreakdownChart.ps1 b/PwshSpectreConsole/public/formatting/Format-SpectreBreakdownChart.ps1 index 8d16b37d..63575ae8 100644 --- a/PwshSpectreConsole/public/formatting/Format-SpectreBreakdownChart.ps1 +++ b/PwshSpectreConsole/public/formatting/Format-SpectreBreakdownChart.ps1 @@ -1,65 +1,67 @@ -using module "..\..\private\completions\Completers.psm1" -using namespace Spectre.Console - -function Format-SpectreBreakdownChart { - <# - .SYNOPSIS - Formats data into a breakdown chart. - ![Example breakdown chart](/breakdownchart.png) - - .DESCRIPTION - This function takes an array of data and formats it into a breakdown chart using BreakdownChart. The chart can be customized with a specified width and color. - - .PARAMETER Data - An array of data to be formatted into a breakdown chart. - - .PARAMETER Width - The width of the chart. Defaults to the width of the console. - - .PARAMETER HideTags - Hides the tags on the chart. - - .PARAMETER HideTagValues - Hides the tag values on the chart. - - .EXAMPLE - $data = @() - - $data += New-SpectreChartItem -Label "Apples" -Value 10 -Color "Green" - $data += New-SpectreChartItem -Label "Oranges" -Value 5 -Color "Gold1" - $data += New-SpectreChartItem -Label "Bananas" -Value 2.2 -Color "#FFFF00" - - Format-SpectreBreakdownChart -Data $data -Width 50 - #> - [Reflection.AssemblyMetadata("title", "Format-SpectreBreakdownChart")] - param ( - [Parameter(ValueFromPipeline, Mandatory)] - [array] $Data, - [ValidateScript({ $_ -gt 0 -and $_ -le (Get-HostWidth) }, ErrorMessage = "Value '{0}' is invalid. Cannot be negative or exceed console width.")] - [int]$Width = (Get-HostWidth), - [switch]$HideTags, - [Switch]$HideTagValues - ) - begin { - $chart = [BreakdownChart]::new() - $chart.Width = $Width - if ($HideTags) { - $chart.ShowTags = $false - } - if ($HideTagValues) { - $chart.ShowTagValues = $false - } - } - process { - if ($Data -is [array]) { - foreach ($dataItem in $Data) { - [BreakdownChartExtensions]::AddItem($chart, $dataItem.Label, $dataItem.Value, ($dataItem.Color | Convert-ToSpectreColor)) | Out-Null - } - } else { - [BreakdownChartExtensions]::AddItem($chart, $Data.Label, $Data.Value, ($Data.Color | Convert-ToSpectreColor)) | Out-Null - } - } - end { - Write-AnsiConsole $chart - } -} +using module "..\..\private\completions\Completers.psm1" +using module "..\..\private\completions\Transformers.psm1" +using namespace Spectre.Console + +function Format-SpectreBreakdownChart { + <# + .SYNOPSIS + Formats data into a breakdown chart. + ![Example breakdown chart](/breakdownchart.png) + + .DESCRIPTION + This function takes an array of data and formats it into a breakdown chart using BreakdownChart. The chart can be customized with a specified width and color. + + .PARAMETER Data + An array of data to be formatted into a breakdown chart. + + .PARAMETER Width + The width of the chart. Defaults to the width of the console. + + .PARAMETER HideTags + Hides the tags on the chart. + + .PARAMETER HideTagValues + Hides the tag values on the chart. + + .EXAMPLE + $data = @() + + $data += New-SpectreChartItem -Label "Apples" -Value 10 -Color "Green" + $data += New-SpectreChartItem -Label "Oranges" -Value 5 -Color "Gold1" + $data += New-SpectreChartItem -Label "Bananas" -Value 2.2 -Color "#FFFF00" + + Format-SpectreBreakdownChart -Data $data -Width 50 + #> + [Reflection.AssemblyMetadata("title", "Format-SpectreBreakdownChart")] + param ( + [Parameter(ValueFromPipeline, Mandatory)] + [ChartItemTransformationAttribute()] + [object] $Data, + [ValidateScript({ $_ -gt 0 -and $_ -le (Get-HostWidth) }, ErrorMessage = "Value '{0}' is invalid. Cannot be negative or exceed console width.")] + [int]$Width = (Get-HostWidth), + [switch]$HideTags, + [Switch]$HideTagValues + ) + begin { + $chart = [BreakdownChart]::new() + $chart.Width = $Width + if ($HideTags) { + $chart.ShowTags = $false + } + if ($HideTagValues) { + $chart.ShowTagValues = $false + } + } + process { + if ($Data -is [array]) { + foreach ($dataItem in $Data) { + [BreakdownChartExtensions]::AddItem($chart, $dataItem.Label, $dataItem.Value, ($dataItem.Color | Convert-ToSpectreColor)) | Out-Null + } + } else { + [BreakdownChartExtensions]::AddItem($chart, $Data.Label, $Data.Value, ($Data.Color | Convert-ToSpectreColor)) | Out-Null + } + } + end { + return $chart + } +} diff --git a/PwshSpectreConsole/public/formatting/Format-SpectreColumns.ps1 b/PwshSpectreConsole/public/formatting/Format-SpectreColumns.ps1 new file mode 100644 index 00000000..5063e1a6 --- /dev/null +++ b/PwshSpectreConsole/public/formatting/Format-SpectreColumns.ps1 @@ -0,0 +1,54 @@ +using module "..\..\private\completions\Completers.psm1" +using namespace Spectre.Console + +function Format-SpectreColumns { + <# + .SYNOPSIS + Renders a collection of renderables in columns to the console. + + .DESCRIPTION + This function creates a spectre columns widget that renders a collection of renderables in autosized columns to the console. + Columns can contain renderable items, see https://spectreconsole.net/widgets/columns for more information. + + .PARAMETER Data + An array of objects containing the data to be displayed in the columns. + #> + [Reflection.AssemblyMetadata("title", "New-SpectreColumn")] + param ( + [Parameter(ValueFromPipeline, Mandatory)] + [array] $Data, + [int] $Padding = 1, + [switch] $Expand + ) + begin { + $columnItems = @() + } + process { + if ($Data -is [array]) { + foreach ($dataItem in $Data) { + if ($dataItem -is [Rendering.Renderable]) { + $columnItems += $dataItem + } elseif ($dataItem -is [string]) { + $columnItems += [Text]::new($dataItem) + } else { + throw "Data item must be a spectre renderable object or string" + } + } + } else { + if ($Data -is [Rendering.Renderable]) { + $columnItems += $Data + } elseif ($Data -is [string]) { + $columnItems += [Text]::new($Data) + } else { + throw "Data item must be a spectre renderable object or string" + } + } + } + end { + $columns = [Columns]::new($columnItems) + $columns.Expand = $Expand + $columns.Padding = [Padding]::new($Padding, $Padding, $Padding, $Padding) + + return $columns + } +} diff --git a/PwshSpectreConsole/public/formatting/Format-SpectreJson.ps1 b/PwshSpectreConsole/public/formatting/Format-SpectreJson.ps1 index 2a632340..bf23fed6 100644 --- a/PwshSpectreConsole/public/formatting/Format-SpectreJson.ps1 +++ b/PwshSpectreConsole/public/formatting/Format-SpectreJson.ps1 @@ -1,4 +1,5 @@ using module "..\..\private\completions\Completers.psm1" +using module "..\..\private\completions\Transformers.psm1" using namespace Spectre.Console function Format-SpectreJson { @@ -17,17 +18,20 @@ function Format-SpectreJson { .PARAMETER Depth The maximum depth of the Json. Default is defined by the version of powershell. - .PARAMETER NoBorder - If specified, the Json will not be surrounded by a border. - - .PARAMETER Border - The border style of the Json. Default is "Rounded". - - .PARAMETER Color - The color of the Json border. Default is the accent color of the script. - - .PARAMETER Title - The title of the Json. + .PARAMETER JsonStyle + A hashtable of Spectre Console color names and values to style the Json output. + e.g. + @{ + MemberStyle = "Yellow" + BracesStyle = "Red" + BracketsStyle = "Orange1" + ColonStyle = "White" + CommaStyle = "White" + StringStyle = "White" + NumberStyle = "Red" + BooleanStyle = "LightSkyBlue1" + NullStyle = "Gray" + } .PARAMETER Width The width of the Json panel. @@ -50,7 +54,7 @@ function Format-SpectreJson { } } ) - Format-SpectreJson -Data $data -Title "Employee Data" -Border "Rounded" -Color "Green" + Format-SpectreJson -Data $data -Color "Green" #> [Reflection.AssemblyMetadata("title", "Format-SpectreJson")] [Alias('fsj')] @@ -58,18 +62,24 @@ function Format-SpectreJson { [Parameter(ValueFromPipeline, Mandatory)] [object] $Data, [int] $Depth, - [string] $Title, - [switch] $NoBorder, - [ValidateSet([SpectreConsoleBoxBorder], ErrorMessage = "Value '{0}' is invalid. Try one of: {1}")] - [string] $Border = "Rounded", - [ColorTransformationAttribute()] - [ArgumentCompletionsSpectreColors()] - [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.")] [int] $Height, - [switch] $Expand + [switch] $Expand, + [ValidateSpectreColorTheme()] + [ColorThemeTransformationAttribute()] + [hashtable] $JsonStyle = @{ + MemberStyle = $script:AccentColor + BracesStyle = [Color]::Red + BracketsStyle = [Color]::Orange1 + ColonStyle = $script:AccentColor + CommaStyle = $script:AccentColor + StringStyle = [Color]::White + NumberStyle = [Color]::Red + BooleanStyle = [Color]::LightSkyBlue1 + NullStyle = $script:DefaultValueColor + } ) begin { $collector = [System.Collections.Generic.List[psobject]]::new() @@ -108,6 +118,7 @@ function Format-SpectreJson { } catch { Write-Debug "Failed to convert json to object, $_" } + } return $collector.add( [pscustomobject]@{ @@ -146,35 +157,17 @@ function Format-SpectreJson { Write-Error "Failed to convert to json, $_" return } - $json.BracesStyle = [Style]::new([Color]::Red) - $json.BracketsStyle = [Style]::new([Color]::Green) - $json.ColonStyle = [Style]::new([Color]::Blue) - $json.CommaStyle = [Style]::new([Color]::CadetBlue) - $json.StringStyle = [Style]::new([Color]::Yellow) - $json.NumberStyle = [Style]::new([Color]::Cyan2) - $json.BooleanStyle = [Style]::new([Color]::Teal) - $json.NullStyle = [Style]::new([Color]::Plum1) - if ($NoBorder) { - Write-AnsiConsole $json - return - } + $json.MemberStyle = $JsonStyle.MemberStyle + $json.BracesStyle = $JsonStyle.BracesStyle + $json.BracketsStyle = $JsonStyle.BracketsStyle + $json.ColonStyle = $JsonStyle.ColonStyle + $json.CommaStyle = $JsonStyle.CommaStyle + $json.StringStyle = $JsonStyle.StringStyle + $json.NumberStyle = $JsonStyle.NumberStyle + $json.BooleanStyle = $JsonStyle.BooleanStyle + $json.NullStyle = $JsonStyle.NullStyle - $panel = [Panel]::new($json) - $panel.Border = [BoxBorder]::$Border - $panel.BorderStyle = [Style]::new($Color) - if ($Title) { - $panel.Header = [PanelHeader]::new($Title) - } - if ($width) { - $panel.Width = $Width - } - if ($height) { - $panel.Height = $Height - } - if ($Expand) { - $panel.Expand = $Expand - } - Write-AnsiConsole $panel + return $json } } diff --git a/PwshSpectreConsole/public/formatting/Format-SpectrePanel.ps1 b/PwshSpectreConsole/public/formatting/Format-SpectrePanel.ps1 index 34deadb0..38ac5dfc 100644 --- a/PwshSpectreConsole/public/formatting/Format-SpectrePanel.ps1 +++ b/PwshSpectreConsole/public/formatting/Format-SpectrePanel.ps1 @@ -1,70 +1,74 @@ -using module "..\..\private\completions\Completers.psm1" -using namespace Spectre.Console - -function Format-SpectrePanel { - <# - .SYNOPSIS - Formats a string as a Spectre Console panel with optional title, border, and color. - ![Spectre panel example](/panel.png) - - .DESCRIPTION - This function takes a string and formats it as a Spectre Console panel with optional title, border, and color. The resulting panel can be displayed in the console using the Write-Host command. - - .PARAMETER Data - The string to be formatted as a panel. - - .PARAMETER Title - The title to be displayed at the top of the panel. - - .PARAMETER Border - The type of border to be displayed around the panel. - - .PARAMETER Expand - Switch parameter that specifies whether the panel should be expanded to fill the available space. - - .PARAMETER Color - The color of the panel border. - - .PARAMETER Width - The width of the panel. - - .PARAMETER Height - The height of the panel. - - .EXAMPLE - Format-SpectrePanel -Data "Hello, world!" -Title "My Panel" -Border "Rounded" -Color "Red" - - .EXAMPLE - Format-SpectrePanel -Data "Hello, big panel!" -Title "My Big Panel" -Border "Double" -Color "Magenta1" -Expand - #> - [Reflection.AssemblyMetadata("title", "Format-SpectrePanel")] - param ( - [Parameter(ValueFromPipeline, Mandatory)] - [object] $Data, - [string] $Title, - [ValidateSet([SpectreConsoleBoxBorder], ErrorMessage = "Value '{0}' is invalid. Try one of: {1}")] - [string] $Border = "Rounded", - [switch] $Expand, - [ColorTransformationAttribute()] - [ArgumentCompletionsSpectreColors()] - [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.")] - [int]$Height - ) - $panel = [Panel]::new($Data) - if ($Title) { - $panel.Header = [PanelHeader]::new($Title) - } - if ($width) { - $panel.Width = $Width - } - if ($height) { - $panel.Height = $Height - } - $panel.Expand = $Expand - $panel.Border = [BoxBorder]::$Border - $panel.BorderStyle = [Style]::new($Color) - Write-AnsiConsole $panel -} +using module "..\..\private\completions\Completers.psm1" +using module "..\..\private\completions\Transformers.psm1" +using namespace Spectre.Console + +function Format-SpectrePanel { + <# + .SYNOPSIS + Formats a string as a Spectre Console panel with optional title, border, and color. + ![Spectre panel example](/panel.png) + + .DESCRIPTION + This function takes a string and formats it as a Spectre Console panel with optional title, border, and color. The resulting panel can be displayed in the console using the Write-Host command. + + .PARAMETER Data + The string to be formatted as a panel. + + .PARAMETER Header + The title to be displayed at the top of the panel. + + .PARAMETER Border + The type of border to be displayed around the panel. + + .PARAMETER Expand + Switch parameter that specifies whether the panel should be expanded to fill the available space. + + .PARAMETER Color + The color of the panel border. + + .PARAMETER Width + The width of the panel. + + .PARAMETER Height + The height of the panel. + + .EXAMPLE + Format-SpectrePanel -Data "Hello, world!" -Title "My Panel" -Border "Rounded" -Color "Red" + + .EXAMPLE + Format-SpectrePanel -Data "Hello, big panel!" -Title "My Big Panel" -Border "Double" -Color "Magenta1" -Expand + #> + [Reflection.AssemblyMetadata("title", "Format-SpectrePanel")] + param ( + [Parameter(ValueFromPipeline, Mandatory)] + [RenderableTransformationAttribute()] + [object] $Data, + [Alias("Title")] + [string] $Header, + [ValidateSet([SpectreConsoleBoxBorder], ErrorMessage = "Value '{0}' is invalid. Try one of: {1}")] + [string] $Border = "Rounded", + [switch] $Expand, + [ColorTransformationAttribute()] + [ArgumentCompletionsSpectreColors()] + [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.")] + [int]$Height + ) + $panel = [Panel]::new($Data) + if ($Header) { + $panel.Header = [PanelHeader]::new($Header) + } + if ($width) { + $panel.Width = $Width + } + if ($height) { + $panel.Height = $Height + } + $panel.Expand = $Expand + $panel.Border = [BoxBorder]::$Border + $panel.BorderStyle = [Style]::new($Color) + + return $panel +} diff --git a/PwshSpectreConsole/public/formatting/Format-SpectreTable.ps1 b/PwshSpectreConsole/public/formatting/Format-SpectreTable.ps1 index f7215e08..1a579df7 100644 --- a/PwshSpectreConsole/public/formatting/Format-SpectreTable.ps1 +++ b/PwshSpectreConsole/public/formatting/Format-SpectreTable.ps1 @@ -1,4 +1,5 @@ using module "..\..\private\completions\Completers.psm1" +using module "..\..\private\completions\Transformers.psm1" using namespace Spectre.Console function Format-SpectreTable { @@ -142,6 +143,7 @@ function Format-SpectreTable { $collector.add($renderableKey) } elseif ($entry -is [hashtable] -or $entry -is [ordered]) { # Recursively expand values in the hashtable finding any renderables and putting them in the lookup table + # Renderables is mutable (hashtables just are) so the Convert-HashtableToRenderSafePSObject will add the renderables to the lookup table $entry = Convert-HashtableToRenderSafePSObject -Hashtable $entry -Renderables $renderables $collector.add($entry) } else { @@ -191,29 +193,6 @@ function Format-SpectreTable { $table.Title = [TableTitle]::new($Title, [Style]::new($Color)) } - if ($PassThru) { - return $table - } else { - Write-AnsiConsole $table - } - } -} - -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 $table } - return [pscustomobject]$customObject } \ No newline at end of file diff --git a/PwshSpectreConsole/public/formatting/Format-SpectreTree.ps1 b/PwshSpectreConsole/public/formatting/Format-SpectreTree.ps1 index 19e67acc..a30042e8 100644 --- a/PwshSpectreConsole/public/formatting/Format-SpectreTree.ps1 +++ b/PwshSpectreConsole/public/formatting/Format-SpectreTree.ps1 @@ -1,65 +1,71 @@ -using module "..\..\private\completions\Completers.psm1" -using namespace Spectre.Console - -function Format-SpectreTree { - <# - .SYNOPSIS - Formats a hashtable as a tree using Spectre Console. - - .DESCRIPTION - This function takes a hashtable and formats it as a tree using Spectre Console. The hashtable should have a 'Label' key and a 'Children' key. The 'Label' key should contain the label for the root node of the tree, and the 'Children' key should contain an array of hashtables representing the child nodes of the root node. Each child hashtable should have a 'Label' key and a 'Children' key, following the same structure as the root node. - - .PARAMETER Data - The hashtable to format as a tree. - - .PARAMETER Border - The type of border to use for the tree. - - .PARAMETER Color - The color to use for the tree. This can be a Spectre Console color name or a hex color code. Default is the accent color defined in the script. - - .EXAMPLE - $data = @{ - Label = "Root" - Children = @( - @{ - Label = "Child 1" - Children = @( - @{ - Label = "Grandchild 1" - Children = @() - }, - @{ - Label = "Grandchild 2" - Children = @() - } - ) - }, - @{ - Label = "Child 2" - Children = @() - } - ) - } - - Format-SpectreTree -Data $data -Guide BoldLine -Color "Green" - #> - [Reflection.AssemblyMetadata("title", "Format-SpectreTree")] - param ( - [Parameter(ValueFromPipeline, Mandatory)] - [hashtable] $Data, - [ValidateSet([SpectreConsoleTreeGuide], ErrorMessage = "Value '{0}' is invalid. Try one of: {1}")] - [string] $Guide = "Line", - [ColorTransformationAttribute()] - [ArgumentCompletionsSpectreColors()] - [Color] $Color = $script:AccentColor - ) - - $tree = [Tree]::new($Data.Label) - $tree.Guide = [TreeGuide]::$Guide - - Add-SpectreTreeNode -Node $tree -Children $Data.Children - - $tree.Style = [Style]::new($Color) - Write-AnsiConsole $tree +using module "..\..\private\completions\Completers.psm1" +using module "..\..\private\completions\Transformers.psm1" +using namespace Spectre.Console + +function Format-SpectreTree { + <# + .SYNOPSIS + Formats a hashtable as a tree using Spectre Console. + + .DESCRIPTION + This function takes a hashtable and formats it as a tree using Spectre Console. The hashtable should have a 'Value' key and a 'Children' key. The 'Value' key should contain the Spectre Console renderable item (text or other objects like calendars etc.) for the node of the tree, and the 'Children' key should contain an array of hashtables representing the child nodes of the node. + + .PARAMETER Data + The hashtable to format as a tree. + + .PARAMETER Guide + The type of line to use for the tree. + + .PARAMETER Color + The color to use for the tree. This can be a Spectre Console color name or a hex color code. Default is the accent color defined in the script. + + .EXAMPLE + $calendar = Write-SpectreCalendar -Date 2024-07-01 -PassThru + $data = @{ + Value = "Root" + Children = @( + @{ + Value = "Child 1" + Children = @( + @{ + Value = "Grandchild 1" + Children = @() + }, + @{ + Value = $calendar + Children = @() + } + ) + }, + @{ + Value = "Child 2" + Children = @() + } + ) + } + + Format-SpectreTree -Data $data -Guide BoldLine -Color "Green" + #> + [Reflection.AssemblyMetadata("title", "Format-SpectreTree")] + param ( + [Parameter(ValueFromPipeline, Mandatory)] + [ValidateSpectreTreeItem()] + [hashtable] $Data, + [ValidateSet([SpectreConsoleTreeGuide], ErrorMessage = "Value '{0}' is invalid. Try one of: {1}")] + [string] $Guide = "Line", + [ColorTransformationAttribute()] + [ArgumentCompletionsSpectreColors()] + [Color] $Color = $script:AccentColor + ) + + $tree = [Tree]::new($Data.Value) + $tree.Guide = [TreeGuide]::$Guide + + if ($Data.Children) { + Add-SpectreTreeNode -Node $tree -Children $Data.Children + } + + $tree.Style = [Style]::new($Color) + + return $tree } \ No newline at end of file diff --git a/PwshSpectreConsole/public/output/Out-SpectreHost.ps1 b/PwshSpectreConsole/public/output/Out-SpectreHost.ps1 new file mode 100644 index 00000000..d645f009 --- /dev/null +++ b/PwshSpectreConsole/public/output/Out-SpectreHost.ps1 @@ -0,0 +1,39 @@ +using module "..\..\private\completions\Transformers.psm1" + +function Out-SpectreHost { + <# + .SYNOPSIS + Writes a spectre renderable to the console host. + + .DESCRIPTION + Out-SpectreHost writes a spectre renderable object to the console host. + This function is used to output spectre renderables to the console when you want to avoid the additional newlines that the PowerShell formatter adds. + + .PARAMETER Data + The data to write to the console. + + .EXAMPLE + $table = New-SpectreTable -Data $data + $table | Out-SpectreHost + #> + [Reflection.AssemblyMetadata("title", "Out-SpectreHost")] + param ( + [Parameter(ValueFromPipeline, Mandatory)] + [RenderableTransformationAttribute()] + [object] $Data + ) + + begin {} + + process { + if ($Data -is [array]) { + foreach ($dataItem in $Data) { + Write-AnsiConsole -RenderableObject $dataItem + } + } else { + Write-AnsiConsole -RenderableObject $Data + } + } + + end {} +} \ No newline at end of file diff --git a/PwshSpectreConsole/public/progress/Invoke-SpectreCommandWithStatus.ps1 b/PwshSpectreConsole/public/progress/Invoke-SpectreCommandWithStatus.ps1 index 6e0746d7..d1e3c510 100644 --- a/PwshSpectreConsole/public/progress/Invoke-SpectreCommandWithStatus.ps1 +++ b/PwshSpectreConsole/public/progress/Invoke-SpectreCommandWithStatus.ps1 @@ -1,58 +1,59 @@ -using module "..\..\private\completions\Completers.psm1" -using namespace Spectre.Console - -function Invoke-SpectreCommandWithStatus { - <# - .SYNOPSIS - Invokes a script block with a Spectre status spinner. - - .DESCRIPTION - This function starts a Spectre status spinner with the specified title and spinner type, and invokes the specified script block. The spinner will continue to spin until the script block completes. - - .PARAMETER ScriptBlock - The script block to invoke. - - .PARAMETER Spinner - The type of spinner to display. - - .PARAMETER Title - The title to display above the spinner. - - .PARAMETER Color - The color of the spinner. Valid values can be found with Get-SpectreDemoColors. - - .EXAMPLE - $result = Invoke-SpectreCommandWithStatus -Spinner "Dots2" -Title "Showing a spinner..." -ScriptBlock { - # Write updates to the host using Write-SpectreHost - Start-Sleep -Seconds 1 - Write-SpectreHost "`n[grey]LOG:[/] Doing some work " - Start-Sleep -Seconds 1 - Write-SpectreHost "`n[grey]LOG:[/] Doing some more work " - Start-Sleep -Seconds 1 - Write-SpectreHost "`n[grey]LOG:[/] Done " - Start-Sleep -Seconds 1 - Write-SpectreHost " " - return "Some result" - } - Write-SpectreHost "Result: $result" - #> - [Reflection.AssemblyMetadata("title", "Invoke-SpectreCommandWithStatus")] - param ( - [Parameter(Mandatory)] - [scriptblock] $ScriptBlock, - [ValidateSet([SpectreConsoleSpinner], ErrorMessage = "Value '{0}' is invalid. Try one of: {1}")] - [string] $Spinner = "Dots", - [Parameter(Mandatory)] - [string] $Title, - [ColorTransformationAttribute()] - [ArgumentCompletionsSpectreColors()] - [Color] $Color = $script:AccentColor - ) - $splat = @{ - Title = $Title - Spinner = [Spinner+Known]::$Spinner - SpinnerStyle = [Style]::new($Color) - ScriptBlock = $ScriptBlock - } - Start-AnsiConsoleStatus @splat -} +using module "..\..\private\completions\Completers.psm1" +using module "..\..\private\completions\Transformers.psm1" +using namespace Spectre.Console + +function Invoke-SpectreCommandWithStatus { + <# + .SYNOPSIS + Invokes a script block with a Spectre status spinner. + + .DESCRIPTION + This function starts a Spectre status spinner with the specified title and spinner type, and invokes the specified script block. The spinner will continue to spin until the script block completes. + + .PARAMETER ScriptBlock + The script block to invoke. + + .PARAMETER Spinner + The type of spinner to display. + + .PARAMETER Title + The title to display above the spinner. + + .PARAMETER Color + The color of the spinner. Valid values can be found with Get-SpectreDemoColors. + + .EXAMPLE + $result = Invoke-SpectreCommandWithStatus -Spinner "Dots2" -Title "Showing a spinner..." -ScriptBlock { + # Write updates to the host using Write-SpectreHost + Start-Sleep -Seconds 1 + Write-SpectreHost "`n[grey]LOG:[/] Doing some work " + Start-Sleep -Seconds 1 + Write-SpectreHost "`n[grey]LOG:[/] Doing some more work " + Start-Sleep -Seconds 1 + Write-SpectreHost "`n[grey]LOG:[/] Done " + Start-Sleep -Seconds 1 + Write-SpectreHost " " + return "Some result" + } + Write-SpectreHost "Result: $result" + #> + [Reflection.AssemblyMetadata("title", "Invoke-SpectreCommandWithStatus")] + param ( + [Parameter(Mandatory)] + [scriptblock] $ScriptBlock, + [ValidateSet([SpectreConsoleSpinner], ErrorMessage = "Value '{0}' is invalid. Try one of: {1}")] + [string] $Spinner = "Dots", + [Parameter(Mandatory)] + [string] $Title, + [ColorTransformationAttribute()] + [ArgumentCompletionsSpectreColors()] + [Color] $Color = $script:AccentColor + ) + $splat = @{ + Title = $Title + Spinner = [Spinner+Known]::$Spinner + SpinnerStyle = [Style]::new($Color) + ScriptBlock = $ScriptBlock + } + Start-AnsiConsoleStatus @splat +} diff --git a/PwshSpectreConsole/public/prompts/Read-SpectreConfirm.ps1 b/PwshSpectreConsole/public/prompts/Read-SpectreConfirm.ps1 index 8d63efc2..52fbf857 100644 --- a/PwshSpectreConsole/public/prompts/Read-SpectreConfirm.ps1 +++ b/PwshSpectreConsole/public/prompts/Read-SpectreConfirm.ps1 @@ -1,71 +1,72 @@ -using module "..\..\private\completions\Completers.psm1" -using namespace Spectre.Console - -function Read-SpectreConfirm { - <# - .SYNOPSIS - Displays a simple confirmation prompt with the option of selecting yes or no and returns a boolean representing the answer. - - .DESCRIPTION - Displays a simple confirmation prompt with the option of selecting yes or no. Additional options are provided to display either a success or failure response message in addition to the boolean return value. - - .PARAMETER Prompt - The prompt to display to the user. The default value is "Do you like cute animals?". - - .PARAMETER DefaultAnswer - The default answer to the prompt if the user just presses enter. The default value is "y". - - .PARAMETER ConfirmSuccess - The text and markup to display if the user chooses yes. If left undefined, nothing will display. - - .PARAMETER ConfirmFailure - The text and markup to display if the user chooses no. If left undefined, nothing will display. - - .EXAMPLE - $answer = Read-SpectreConfirm -Prompt "Would you like to continue the preview installation of [#7693FF]PowerShell 7?[/]" ` - -ConfirmSuccess "Woohoo! The internet awaits your elite development contributions." ` - -ConfirmFailure "What kind of monster are you? How could you do this?" - # Type "y", "↲" to accept the prompt - Write-Host "Your answer was '$answer'" - #> - [Reflection.AssemblyMetadata("title", "Read-SpectreConfirm")] - param ( - [String] $Prompt = "Do you like cute animals?", - [ValidateSet("y", "n", "none")] - [string] $DefaultAnswer = "y", - [string] $ConfirmSuccess, - [string] $ConfirmFailure, - [ColorTransformationAttribute()] - [ArgumentCompletionsSpectreColors()] - [Color] $Color = $script:AccentColor - ) - - # This is much fiddlier but it exposes the ability to set the color scheme. The confirmationprompt is just a textprompt with two choices hard coded to y/n: - # https://github.com/spectreconsole/spectre.console/blob/63b940cf0eb127a8cd891a4fe338aa5892d502c5/src/Spectre.Console/Prompts/ConfirmationPrompt.cs#L71 - $confirmationPrompt = [TextPrompt[string]]::new($Prompt) - $confirmationPrompt = [TextPromptExtensions]::AddChoice($confirmationPrompt, "y") - $confirmationPrompt = [TextPromptExtensions]::AddChoice($confirmationPrompt, "n") - if ($DefaultAnswer -ne "none") { - $confirmationPrompt = [TextPromptExtensions]::DefaultValue($confirmationPrompt, $DefaultAnswer) - } - - # This is how I added the default colors with Set-SpectreColors so you don't have to keep passing them through and get a consistent TUI color scheme - $confirmationPrompt.DefaultValueStyle = [Style]::new($script:DefaultValueColor) - $confirmationPrompt.ChoicesStyle = [Style]::new($Color) - $confirmationPrompt.InvalidChoiceMessage = "[red]Please select one of the available options[/]" - - # Invoke-SpectrePromptAsync supports ctrl-c - $confirmed = (Invoke-SpectrePromptAsync -Prompt $confirmationPrompt) -eq "y" - - if (!$confirmed) { - if (![String]::IsNullOrWhiteSpace($ConfirmFailure)) { - Write-SpectreHost $ConfirmFailure - } - } else { - if (![String]::IsNullOrWhiteSpace($ConfirmSuccess)) { - Write-SpectreHost $ConfirmSuccess - } - } - - return $confirmed +using module "..\..\private\completions\Completers.psm1" +using module "..\..\private\completions\Transformers.psm1" +using namespace Spectre.Console + +function Read-SpectreConfirm { + <# + .SYNOPSIS + Displays a simple confirmation prompt with the option of selecting yes or no and returns a boolean representing the answer. + + .DESCRIPTION + Displays a simple confirmation prompt with the option of selecting yes or no. Additional options are provided to display either a success or failure response message in addition to the boolean return value. + + .PARAMETER Prompt + The prompt to display to the user. The default value is "Do you like cute animals?". + + .PARAMETER DefaultAnswer + The default answer to the prompt if the user just presses enter. The default value is "y". + + .PARAMETER ConfirmSuccess + The text and markup to display if the user chooses yes. If left undefined, nothing will display. + + .PARAMETER ConfirmFailure + The text and markup to display if the user chooses no. If left undefined, nothing will display. + + .EXAMPLE + $answer = Read-SpectreConfirm -Prompt "Would you like to continue the preview installation of [#7693FF]PowerShell 7?[/]" ` + -ConfirmSuccess "Woohoo! The internet awaits your elite development contributions." ` + -ConfirmFailure "What kind of monster are you? How could you do this?" + # Type "y", "↲" to accept the prompt + Write-Host "Your answer was '$answer'" + #> + [Reflection.AssemblyMetadata("title", "Read-SpectreConfirm")] + param ( + [String] $Prompt = "Do you like cute animals?", + [ValidateSet("y", "n", "none")] + [string] $DefaultAnswer = "y", + [string] $ConfirmSuccess, + [string] $ConfirmFailure, + [ColorTransformationAttribute()] + [ArgumentCompletionsSpectreColors()] + [Color] $Color = $script:AccentColor + ) + + # This is much fiddlier but it exposes the ability to set the color scheme. The confirmationprompt is just a textprompt with two choices hard coded to y/n: + # https://github.com/spectreconsole/spectre.console/blob/63b940cf0eb127a8cd891a4fe338aa5892d502c5/src/Spectre.Console/Prompts/ConfirmationPrompt.cs#L71 + $confirmationPrompt = [TextPrompt[string]]::new($Prompt) + $confirmationPrompt = [TextPromptExtensions]::AddChoice($confirmationPrompt, "y") + $confirmationPrompt = [TextPromptExtensions]::AddChoice($confirmationPrompt, "n") + if ($DefaultAnswer -ne "none") { + $confirmationPrompt = [TextPromptExtensions]::DefaultValue($confirmationPrompt, $DefaultAnswer) + } + + # This is how I added the default colors with Set-SpectreColors so you don't have to keep passing them through and get a consistent TUI color scheme + $confirmationPrompt.DefaultValueStyle = [Style]::new($script:DefaultValueColor) + $confirmationPrompt.ChoicesStyle = [Style]::new($Color) + $confirmationPrompt.InvalidChoiceMessage = "[red]Please select one of the available options[/]" + + # Invoke-SpectrePromptAsync supports ctrl-c + $confirmed = (Invoke-SpectrePromptAsync -Prompt $confirmationPrompt) -eq "y" + + if (!$confirmed) { + if (![String]::IsNullOrWhiteSpace($ConfirmFailure)) { + Write-SpectreHost $ConfirmFailure + } + } else { + if (![String]::IsNullOrWhiteSpace($ConfirmSuccess)) { + Write-SpectreHost $ConfirmSuccess + } + } + + return $confirmed } \ No newline at end of file diff --git a/PwshSpectreConsole/public/prompts/Read-SpectreMultiSelection.ps1 b/PwshSpectreConsole/public/prompts/Read-SpectreMultiSelection.ps1 index f9bab1ca..0c686c2e 100644 --- a/PwshSpectreConsole/public/prompts/Read-SpectreMultiSelection.ps1 +++ b/PwshSpectreConsole/public/prompts/Read-SpectreMultiSelection.ps1 @@ -1,4 +1,5 @@ using module "..\..\private\completions\Completers.psm1" +using module "..\..\private\completions\Transformers.psm1" using namespace Spectre.Console function Read-SpectreMultiSelection { diff --git a/PwshSpectreConsole/public/prompts/Read-SpectreMultiSelectionGrouped.ps1 b/PwshSpectreConsole/public/prompts/Read-SpectreMultiSelectionGrouped.ps1 index 3c2b0c91..fe0a7a41 100644 --- a/PwshSpectreConsole/public/prompts/Read-SpectreMultiSelectionGrouped.ps1 +++ b/PwshSpectreConsole/public/prompts/Read-SpectreMultiSelectionGrouped.ps1 @@ -1,4 +1,5 @@ using module "..\..\private\completions\Completers.psm1" +using module "..\..\private\completions\Transformers.psm1" using namespace Spectre.Console function Read-SpectreMultiSelectionGrouped { diff --git a/PwshSpectreConsole/public/prompts/Read-SpectreSelection.ps1 b/PwshSpectreConsole/public/prompts/Read-SpectreSelection.ps1 index 8e63b616..d2b664eb 100644 --- a/PwshSpectreConsole/public/prompts/Read-SpectreSelection.ps1 +++ b/PwshSpectreConsole/public/prompts/Read-SpectreSelection.ps1 @@ -1,4 +1,5 @@ using module "..\..\private\completions\Completers.psm1" +using module "..\..\private\completions\Transformers.psm1" using namespace Spectre.Console function Read-SpectreSelection { diff --git a/PwshSpectreConsole/public/prompts/Read-SpectreText.ps1 b/PwshSpectreConsole/public/prompts/Read-SpectreText.ps1 index 0d74db14..fb965906 100644 --- a/PwshSpectreConsole/public/prompts/Read-SpectreText.ps1 +++ b/PwshSpectreConsole/public/prompts/Read-SpectreText.ps1 @@ -1,71 +1,72 @@ -using module "..\..\private\completions\Completers.psm1" -using namespace Spectre.Console - -function Read-SpectreText { - <# - .SYNOPSIS - Prompts the user with a question and returns the user's input. - - .DESCRIPTION - This function uses Spectre Console to prompt the user with a question and returns the user's input. - :::caution - I would advise against this and instead use `Read-Host` because the Spectre Console prompt doesn't have access to the PowerShell session history. - Without session history you can't use the up and down arrow keys to navigate through your previous commands. - This text entry also doesn't allow you to use arrow keys to go back and forwards through the text you're entering. - ::: - - .PARAMETER Question - The question to prompt the user with. - - .PARAMETER DefaultAnswer - The default answer if the user does not provide any input. - - .PARAMETER AnswerColor - The color of the user's answer input. The default behaviour uses the standard terminal text color. - - .PARAMETER AllowEmpty - If specified, the user can provide an empty answer. - - .PARAMETER Choices - An array of choices that the user can choose from. If specified, the user will be prompted with a list of choices to choose from, with validation. - With autocomplete and can tab through the choices. - - .EXAMPLE - $name = Read-SpectreText -Question "What's your name?" -DefaultAnswer "Prefer not to say" - # Type "↲" to provide no answer - Write-SpectreHost "Your name is '$name'" - - .EXAMPLE - $favouriteColor = Read-SpectreText -Question "What's your favorite color?" -DefaultAnswer "pink" - # Type "orange", "↲" to enter your favourite color - Write-SpectreHost "Your favourite color is '$favouriteColor'" - - .EXAMPLE - $favouriteColor = Read-SpectreText -Question "What's your favorite color?" -AnswerColor "Cyan1" -Choices "Black", "Green", "Magenta", "I'll never tell!" - # Type "orange", "↲", "magenta", "↲" to enter text that must match a choice in the choices list, orange will be rejected, magenta will be accepted - Write-SpectreHost "Your favourite color is '$favouriteColor'" - #> - [Reflection.AssemblyMetadata("title", "Read-SpectreText")] - param ( - [string] $Question = "What's your name?", - [string] $DefaultAnswer, - [ColorTransformationAttribute()] - [ArgumentCompletionsSpectreColors()] - [Color] $AnswerColor, - [switch] $AllowEmpty, - [string[]] $Choices - ) - $spectrePrompt = [TextPrompt[string]]::new($Question, [System.StringComparer]::InvariantCultureIgnoreCase) - $spectrePrompt.DefaultValueStyle = [Style]::new($script:DefaultValueColor) - if ($DefaultAnswer) { - $spectrePrompt = [TextPromptExtensions]::DefaultValue($spectrePrompt, $DefaultAnswer) - } - if ($AnswerColor) { - $spectrePrompt.PromptStyle = [Style]::new($AnswerColor) - } - $spectrePrompt.AllowEmpty = $AllowEmpty - if ($null -ne $Choices) { - $spectrePrompt = [TextPromptExtensions]::AddChoices($spectrePrompt, $Choices) - } - return Invoke-SpectrePromptAsync -Prompt $spectrePrompt -} +using module "..\..\private\completions\Completers.psm1" +using module "..\..\private\completions\Transformers.psm1" +using namespace Spectre.Console + +function Read-SpectreText { + <# + .SYNOPSIS + Prompts the user with a question and returns the user's input. + + .DESCRIPTION + This function uses Spectre Console to prompt the user with a question and returns the user's input. + :::caution + I would advise against this and instead use `Read-Host` because the Spectre Console prompt doesn't have access to the PowerShell session history. + Without session history you can't use the up and down arrow keys to navigate through your previous commands. + This text entry also doesn't allow you to use arrow keys to go back and forwards through the text you're entering. + ::: + + .PARAMETER Question + The question to prompt the user with. + + .PARAMETER DefaultAnswer + The default answer if the user does not provide any input. + + .PARAMETER AnswerColor + The color of the user's answer input. The default behaviour uses the standard terminal text color. + + .PARAMETER AllowEmpty + If specified, the user can provide an empty answer. + + .PARAMETER Choices + An array of choices that the user can choose from. If specified, the user will be prompted with a list of choices to choose from, with validation. + With autocomplete and can tab through the choices. + + .EXAMPLE + $name = Read-SpectreText -Question "What's your name?" -DefaultAnswer "Prefer not to say" + # Type "↲" to provide no answer + Write-SpectreHost "Your name is '$name'" + + .EXAMPLE + $favouriteColor = Read-SpectreText -Question "What's your favorite color?" -DefaultAnswer "pink" + # Type "orange", "↲" to enter your favourite color + Write-SpectreHost "Your favourite color is '$favouriteColor'" + + .EXAMPLE + $favouriteColor = Read-SpectreText -Question "What's your favorite color?" -AnswerColor "Cyan1" -Choices "Black", "Green", "Magenta", "I'll never tell!" + # Type "orange", "↲", "magenta", "↲" to enter text that must match a choice in the choices list, orange will be rejected, magenta will be accepted + Write-SpectreHost "Your favourite color is '$favouriteColor'" + #> + [Reflection.AssemblyMetadata("title", "Read-SpectreText")] + param ( + [string] $Question = "What's your name?", + [string] $DefaultAnswer, + [ColorTransformationAttribute()] + [ArgumentCompletionsSpectreColors()] + [Color] $AnswerColor, + [switch] $AllowEmpty, + [string[]] $Choices + ) + $spectrePrompt = [TextPrompt[string]]::new($Question, [System.StringComparer]::InvariantCultureIgnoreCase) + $spectrePrompt.DefaultValueStyle = [Style]::new($script:DefaultValueColor) + if ($DefaultAnswer) { + $spectrePrompt = [TextPromptExtensions]::DefaultValue($spectrePrompt, $DefaultAnswer) + } + if ($AnswerColor) { + $spectrePrompt.PromptStyle = [Style]::new($AnswerColor) + } + $spectrePrompt.AllowEmpty = $AllowEmpty + if ($null -ne $Choices) { + $spectrePrompt = [TextPromptExtensions]::AddChoices($spectrePrompt, $Choices) + } + return Invoke-SpectrePromptAsync -Prompt $spectrePrompt +} diff --git a/PwshSpectreConsole/public/writing/Write-SpectreCalender.ps1 b/PwshSpectreConsole/public/writing/Write-SpectreCalendar.ps1 similarity index 82% rename from PwshSpectreConsole/public/writing/Write-SpectreCalender.ps1 rename to PwshSpectreConsole/public/writing/Write-SpectreCalendar.ps1 index 09ab7a18..4293552f 100644 --- a/PwshSpectreConsole/public/writing/Write-SpectreCalender.ps1 +++ b/PwshSpectreConsole/public/writing/Write-SpectreCalendar.ps1 @@ -1,4 +1,5 @@ using module "..\..\private\completions\Completers.psm1" +using module "..\..\private\completions\Transformers.psm1" using namespace Spectre.Console function Write-SpectreCalendar { @@ -53,7 +54,8 @@ function Write-SpectreCalendar { [string] $Border = "Double", [cultureinfo] $Culture = [cultureinfo]::CurrentCulture, [Hashtable]$Events, - [Switch] $HideHeader + [Switch] $HideHeader, + [Switch] $PassThru ) $calendar = [Calendar]::new($date) $calendar.Alignment = [Justify]::$Alignment @@ -65,15 +67,21 @@ function Write-SpectreCalendar { if ($HideHeader) { $calendar.ShowHeader = $false } + + $outputData = @($calendar) + if ($Events) { foreach ($event in $events.GetEnumerator()) { - # calendar events doesnt appear to support Culture. + # Calendar events don't appear to support Culture. $eventDate = $event.Name -as [datetime] $calendar = [CalendarExtensions]::AddCalendarEvent($calendar, $event.value, $eventDate.Year, $eventDate.Month, $eventDate.Day) } - Write-AnsiConsole $calendar - $calendar.CalendarEvents | Sort-Object -Property Day | Format-SpectreTable -Border $Border -Color $Color - } else { - Write-AnsiConsole $calendar + $outputData += $calendar.CalendarEvents | Sort-Object -Property Day | Format-SpectreTable -Border $Border -Color $Color + } + + if ($PassThru) { + return $outputData } + + $outputData | Out-SpectreHost } diff --git a/PwshSpectreConsole/public/writing/Write-SpectreFigletText.ps1 b/PwshSpectreConsole/public/writing/Write-SpectreFigletText.ps1 index fcf03176..02475e62 100644 --- a/PwshSpectreConsole/public/writing/Write-SpectreFigletText.ps1 +++ b/PwshSpectreConsole/public/writing/Write-SpectreFigletText.ps1 @@ -1,46 +1,53 @@ -using module "..\..\private\completions\Completers.psm1" -using namespace Spectre.Console - -function Write-SpectreFigletText { - <# - .SYNOPSIS - Writes a Spectre Console Figlet text to the console. - - .DESCRIPTION - This function writes a Spectre Console Figlet text to the console. The text can be aligned to the left, right, or center, and can be displayed in a specified color. - - .PARAMETER Text - The text to display in the Figlet format. - - .PARAMETER Alignment - The alignment of the text. The default value is "Left". - - .PARAMETER Color - The color of the text. The default value is the accent color of the script. - - .PARAMETER FigletFontPath - The path to the Figlet font file to use. If this parameter is not specified, the default built-in Figlet font is used. - The figlet font format is usually *.flf, see https://spectreconsole.net/widgets/figlet for more. - - .EXAMPLE - Write-SpectreFigletText -Text "Hello Spectre!" -Alignment "Center" -Color "Red" - - .EXAMPLE - Write-SpectreFigletText -Text "Whoa?!" -FigletFontPath "..\assets\3d.flf" - #> - [Reflection.AssemblyMetadata("title", "Write-SpectreFigletText")] - param ( - [string] $Text = "Hello Spectre!", - [ValidateSet([SpectreConsoleJustify], ErrorMessage = "Value '{0}' is invalid. Try one of: {1}")] - [string] $Alignment = "Left", - [ColorTransformationAttribute()] - [ArgumentCompletionsSpectreColors()] - [Color] $Color = $script:AccentColor, - [string] $FigletFontPath - ) - $figletFont = Read-FigletFont -FigletFontPath $FigletFontPath - $figletText = [FigletText]::new($figletFont, $Text) - $figletText.Justification = [Justify]::$Alignment - $figletText.Color = $Color - Write-AnsiConsole $figletText +using module "..\..\private\completions\Completers.psm1" +using module "..\..\private\completions\Transformers.psm1" +using namespace Spectre.Console + +function Write-SpectreFigletText { + <# + .SYNOPSIS + Writes a Spectre Console Figlet text to the console. + + .DESCRIPTION + This function writes a Spectre Console Figlet text to the console. The text can be aligned to the left, right, or center, and can be displayed in a specified color. + + .PARAMETER Text + The text to display in the Figlet format. + + .PARAMETER Alignment + The alignment of the text. The default value is "Left". + + .PARAMETER Color + The color of the text. The default value is the accent color of the script. + + .PARAMETER FigletFontPath + The path to the Figlet font file to use. If this parameter is not specified, the default built-in Figlet font is used. + The figlet font format is usually *.flf, see https://spectreconsole.net/widgets/figlet for more. + + .EXAMPLE + Write-SpectreFigletText -Text "Hello Spectre!" -Alignment "Center" -Color "Red" + + .EXAMPLE + Write-SpectreFigletText -Text "Whoa?!" -FigletFontPath "..\assets\3d.flf" + #> + [Reflection.AssemblyMetadata("title", "Write-SpectreFigletText")] + param ( + [string] $Text = "Hello Spectre!", + [ValidateSet([SpectreConsoleJustify], ErrorMessage = "Value '{0}' is invalid. Try one of: {1}")] + [string] $Alignment = "Left", + [ColorTransformationAttribute()] + [ArgumentCompletionsSpectreColors()] + [Color] $Color = $script:AccentColor, + [string] $FigletFontPath, + [switch] $PassThru + ) + $figletFont = Read-FigletFont -FigletFontPath $FigletFontPath + $figletText = [FigletText]::new($figletFont, $Text) + $figletText.Justification = [Justify]::$Alignment + $figletText.Color = $Color + + if ($PassThru) { + return $figletText + } + + Write-AnsiConsole $figletText } \ No newline at end of file diff --git a/PwshSpectreConsole/public/writing/Write-SpectreHost.ps1 b/PwshSpectreConsole/public/writing/Write-SpectreHost.ps1 index a588e175..7660f832 100644 --- a/PwshSpectreConsole/public/writing/Write-SpectreHost.ps1 +++ b/PwshSpectreConsole/public/writing/Write-SpectreHost.ps1 @@ -1,33 +1,44 @@ -function Write-SpectreHost { - <# - .SYNOPSIS - Writes a message to the console using Spectre Console markup. - - .DESCRIPTION - The Write-SpectreHost function writes a message to the console using Spectre Console. It supports ANSI markup and can optionally append a newline character to the end of the message. - The markup language is defined at [https://spectreconsole.net/markup](https://spectreconsole.net/markup) - Supported emoji are defined at [https://spectreconsole.net/appendix/emojis](https://spectreconsole.net/appendix/emojis) - - .PARAMETER Message - The message to write to the console. - - .PARAMETER NoNewline - If specified, the message will not be followed by a newline character. - - .EXAMPLE - Write-SpectreHost -Message "Hello, [blue underline]world[/]! :call_me_hand:" - #> - [Reflection.AssemblyMetadata("title", "Write-SpectreHost")] - [Reflection.AssemblyMetadata("description", "The Write-SpectreHost function writes a message to the console using Spectre Console. It supports ANSI markup and can optionally append a newline character to the end of the message.")] - param ( - [Parameter(ValueFromPipeline, Mandatory)] - [string] $Message, - [switch] $NoNewline - ) - - if ($NoNewline) { - Write-SpectreHostInternalMarkup $Message - } else { - Write-SpectreHostInternalMarkupLine $Message - } +using namespace Spectre.Console + +function Write-SpectreHost { + <# + .SYNOPSIS + Writes a message to the console using Spectre Console markup. + + .DESCRIPTION + The Write-SpectreHost function writes a message to the console using Spectre Console. It supports ANSI markup and can optionally append a newline character to the end of the message. + The markup language is defined at [https://spectreconsole.net/markup](https://spectreconsole.net/markup) + Supported emoji are defined at [https://spectreconsole.net/appendix/emojis](https://spectreconsole.net/appendix/emojis) + + .PARAMETER Message + The message to write to the console. + + .PARAMETER NoNewline + If specified, the message will not be followed by a newline character. + + .EXAMPLE + Write-SpectreHost -Message "Hello, [blue underline]world[/]! :call_me_hand:" + #> + [Reflection.AssemblyMetadata("title", "Write-SpectreHost")] + param ( + [Parameter(ValueFromPipeline, Mandatory)] + [object] $Message, + [switch] $NoNewline, + [switch] $PassThru + ) + + if ($PassThru) { + return $Message + } + + if ($Message -is [Rendering.Renderable]) { + Write-AnsiConsole $Message + return + } + + if ($NoNewline) { + Write-SpectreHostInternalMarkup $Message + } else { + Write-SpectreHostInternalMarkupLine $Message + } } \ No newline at end of file diff --git a/PwshSpectreConsole/public/writing/Write-SpectreRule.ps1 b/PwshSpectreConsole/public/writing/Write-SpectreRule.ps1 index 44be2c66..5c2f218e 100644 --- a/PwshSpectreConsole/public/writing/Write-SpectreRule.ps1 +++ b/PwshSpectreConsole/public/writing/Write-SpectreRule.ps1 @@ -1,37 +1,44 @@ -using module "..\..\private\completions\Completers.psm1" -using namespace Spectre.Console - -function Write-SpectreRule { - <# - .SYNOPSIS - Writes a Spectre horizontal-rule to the console. - - .DESCRIPTION - The Write-SpectreRule function writes a Spectre horizontal-rule to the console with the specified title, alignment, and color. - - .PARAMETER Title - The title of the rule. - - .PARAMETER Alignment - The alignment of the text in the rule. The default value is Left. - - .PARAMETER Color - The color of the rule. The default value is the accent color of the script. - - .EXAMPLE - Write-SpectreRule -Title "My Rule" -Alignment Center -Color Red - #> - [Reflection.AssemblyMetadata("title", "Write-SpectreRule")] - param ( - [Parameter(Mandatory)] - [string] $Title, - [ValidateSet([SpectreConsoleJustify], ErrorMessage = "Value '{0}' is invalid. Try one of: {1}")] - [string] $Alignment = "Left", - [ColorTransformationAttribute()] - [ArgumentCompletionsSpectreColors()] - [Color] $Color = $script:AccentColor - ) - $rule = [Rule]::new("[$($Color.ToMarkup())]$Title[/]") - $rule.Justification = [Justify]::$Alignment - Write-AnsiConsole $rule +using module "..\..\private\completions\Completers.psm1" +using module "..\..\private\completions\Transformers.psm1" +using namespace Spectre.Console + +function Write-SpectreRule { + <# + .SYNOPSIS + Writes a Spectre horizontal-rule to the console. + + .DESCRIPTION + The Write-SpectreRule function writes a Spectre horizontal-rule to the console with the specified title, alignment, and color. + + .PARAMETER Title + The title of the rule. + + .PARAMETER Alignment + The alignment of the text in the rule. The default value is Left. + + .PARAMETER Color + The color of the rule. The default value is the accent color of the script. + + .EXAMPLE + Write-SpectreRule -Title "My Rule" -Alignment Center -Color Red + #> + [Reflection.AssemblyMetadata("title", "Write-SpectreRule")] + param ( + [Parameter(Mandatory)] + [string] $Title, + [ValidateSet([SpectreConsoleJustify], ErrorMessage = "Value '{0}' is invalid. Try one of: {1}")] + [string] $Alignment = "Left", + [ColorTransformationAttribute()] + [ArgumentCompletionsSpectreColors()] + [Color] $Color = $script:AccentColor, + [switch] $PassThru + ) + $rule = [Rule]::new("[$($Color.ToMarkup())]$Title[/]") + $rule.Justification = [Justify]::$Alignment + + if ($PassThru) { + return $rule + } + + Write-AnsiConsole $rule } \ No newline at end of file