public/PwshSpectreConsole.ps1
$script:AccentColor = [Spectre.Console.Color]::Blue $script:DefaultValueColor = [Spectre.Console.Color]::Grey $script:Colors = @("Aqua", "Aquamarine1", "Aquamarine1_1", "Aquamarine3", "Black", "Blue", "Blue1", "Blue3", "Blue3_1", "BlueViolet", "CadetBlue", "CadetBlue_1", "Chartreuse1", "Chartreuse2", "Chartreuse2_1", "Chartreuse3", "Chartreuse3_1", "Chartreuse4", "CornflowerBlue", "Cornsilk1", "Cyan1", "Cyan2", "Cyan3", "DarkBlue", "DarkCyan", "DarkGoldenrod", "DarkGreen", "DarkKhaki", "DarkMagenta", "DarkMagenta_1", "DarkOliveGreen1", "DarkOliveGreen1_1", "DarkOliveGreen2", "DarkOliveGreen3", "DarkOliveGreen3_1", "DarkOliveGreen3_2", "DarkOrange", "DarkOrange3", "DarkOrange3_1", "DarkRed", "DarkRed_1", "DarkSeaGreen", "DarkSeaGreen1", "DarkSeaGreen1_1", "DarkSeaGreen2", "DarkSeaGreen2_1", "DarkSeaGreen3", "DarkSeaGreen3_1", "DarkSeaGreen4", "DarkSeaGreen4_1", "DarkSlateGray1", "DarkSlateGray2", "DarkSlateGray3", "DarkTurquoise", "DarkViolet", "DarkViolet_1", "DeepPink1", "DeepPink1_1", "DeepPink2", "DeepPink3", "DeepPink3_1", "DeepPink4", "DeepPink4_1", "DeepPink4_2", "DeepSkyBlue1", "DeepSkyBlue2", "DeepSkyBlue3", "DeepSkyBlue3_1", "DeepSkyBlue4", "DeepSkyBlue4_1", "DeepSkyBlue4_2", "Default", "DodgerBlue1", "DodgerBlue2", "DodgerBlue3", "Fuchsia", "Gold1", "Gold3", "Gold3_1", "Green", "Green1", "Green3", "Green3_1", "Green4", "GreenYellow", "Grey", "Grey0", "Grey100", "Grey11", "Grey15", "Grey19", "Grey23", "Grey27", "Grey3", "Grey30", "Grey35", "Grey37", "Grey39", "Grey42", "Grey46", "Grey50", "Grey53", "Grey54", "Grey58", "Grey62", "Grey63", "Grey66", "Grey69", "Grey7", "Grey70", "Grey74", "Grey78", "Grey82", "Grey84", "Grey85", "Grey89", "Grey93", "Honeydew2", "HotPink", "HotPink2", "HotPink3", "HotPink3_1", "HotPink_1", "IndianRed", "IndianRed1", "IndianRed1_1", "IndianRed_1", "Khaki1", "Khaki3", "LightCoral", "LightCyan1", "LightCyan3", "LightGoldenrod1", "LightGoldenrod2", "LightGoldenrod2_1", "LightGoldenrod2_2", "LightGoldenrod3", "LightGreen", "LightGreen_1", "LightPink1", "LightPink3", "LightPink4", "LightSalmon1", "LightSalmon3", "LightSalmon3_1", "LightSeaGreen", "LightSkyBlue1", "LightSkyBlue3", "LightSkyBlue3_1", "LightSlateBlue", "LightSlateGrey", "LightSteelBlue", "LightSteelBlue1", "LightSteelBlue3", "LightYellow3", "Lime", "Magenta1", "Magenta2", "Magenta2_1", "Magenta3", "Magenta3_1", "Magenta3_2", "Maroon", "MediumOrchid", "MediumOrchid1", "MediumOrchid1_1", "MediumOrchid3", "MediumPurple", "MediumPurple1", "MediumPurple2", "MediumPurple2_1", "MediumPurple3", "MediumPurple3_1", "MediumPurple4", "MediumSpringGreen", "MediumTurquoise", "MediumVioletRed", "MistyRose1", "MistyRose3", "NavajoWhite1", "NavajoWhite3", "Navy", "NavyBlue", "Olive", "Orange1", "Orange3", "Orange4", "Orange4_1", "OrangeRed1", "Orchid", "Orchid1", "Orchid2", "PaleGreen1", "PaleGreen1_1", "PaleGreen3", "PaleGreen3_1", "PaleTurquoise1", "PaleTurquoise4", "PaleVioletRed1", "Pink1", "Pink3", "Plum1", "Plum2", "Plum3", "Plum4", "Purple", "Purple3", "Purple4", "Purple4_1", "Purple_1", "Purple_2", "Red", "Red1", "Red3", "Red3_1", "RosyBrown", "RoyalBlue1", "Salmon1", "SandyBrown", "SeaGreen1", "SeaGreen1_1", "SeaGreen2", "SeaGreen3", "Silver", "SkyBlue1", "SkyBlue2", "SkyBlue3", "SlateBlue1", "SlateBlue3", "SlateBlue3_1", "SpringGreen1", "SpringGreen2", "SpringGreen2_1", "SpringGreen3", "SpringGreen3_1", "SpringGreen4", "SteelBlue", "SteelBlue1", "SteelBlue1_1", "SteelBlue3", "Tan", "Teal", "Thistle1", "Thistle3", "Turquoise2", "Turquoise4", "Violet", "Wheat1", "Wheat4", "White", "Yellow", "Yellow1", "Yellow2", "Yellow3", "Yellow3_1", "Yellow4", "Yellow4_1") function Invoke-SpectrePromptAsync { param ( $Prompt ) $cts = [System.Threading.CancellationTokenSource]::new() try { $task = $Prompt.ShowAsync([Spectre.Console.AnsiConsole]::Console, $cts.Token) while (-not $task.AsyncWaitHandle.WaitOne(200)) { <# Waiting for async task while allowing ctrl-c interrupts #> } return $task.GetAwaiter().GetResult() } finally { $cts.Cancel() $task.Dispose() } } function Set-SpectreColors { param ( [ValidateScript({if($script:Colors -contains $_) { $true } else { throw "Color must be one of $($script:Colors -join ', ')" } })] [string] $AccentColor = "Blue", [ValidateScript({if($script:Colors -contains $_) { $true } else { throw "Color must be one of $($script:Colors -join ', ')" } })] [string] $DefaultValueColor = "Grey" ) $script:AccentColor = [Spectre.Console.Color]::$AccentColor $script:DefaultValueColor = [Spectre.Console.Color]::$DefaultValueColor } function Write-SpectreParagraph { param ( [string] $Text = "This is a sample paragraph! Provide some text to the command to write your own. This function writes a body of text without splitting a word across multiple lines." ) $windowWidth = $Host.UI.RawUI.WindowSize.Width - 1 $textWithLineBreaks = (Select-String -Input $Text -Pattern ".{1,$windowWidth}(\s|$)" -AllMatches).Matches.Value $textWithLineBreaks | Foreach-Object { Write-Host $_ } } function Write-SpectreRule { param ( [string] $Title, [string] $Alignment = "Left", [ValidateScript({if($script:Colors -contains $_) { $true } else { throw "Color must be one of $($script:Colors -join ', ')" } })] [string] $Color = $script:AccentColor.ToString() ) $rule = [Spectre.Console.Rule]::new("[$($Color)]$Title[/]") $rule.Justification = [Spectre.Console.Justify]::$Alignment [Spectre.Console.AnsiConsole]::Write($rule) } function Write-SpectreFigletText { param ( [string] $Text = "Hello Spectre!", [string] $Alignment = "Left", [ValidateScript({if($script:Colors -contains $_) { $true } else { throw "Color must be one of $($script:Colors -join ', ')" } })] [string] $Color = $script:AccentColor.ToString() ) $figletText = [Spectre.Console.FigletText]::new($Text) $figletText.Justification = switch($Alignment) { "Left" { [Spectre.Console.Justify]::Left } "Right" { [Spectre.Console.Justify]::Right } "Centered" { [Spectre.Console.Justify]::Center } default { Write-Error "Invalid alignment $Alignment" } } $figletText.Color = [Spectre.Console.Color]::$Color [Spectre.Console.AnsiConsole]::Write($figletText) } function Read-SpectreSelection { param ( [string] $Title = "What's your favourite colour [$($script:AccentColor.ToString())]option[/]?", [array] $Choices = @("red", "green", "blue"), [string] $ChoiceLabelProperty, [ValidateScript({if($script:Colors -contains $_) { $true } else { throw "Color must be one of $($script:Colors -join ', ')" } })] [string] $Color = $script:AccentColor.ToString(), [int] $PageSize = 5 ) $prompt = [Spectre.Console.SelectionPrompt[string]]::new() $choiceLabels = $Choices if($ChoiceLabelProperty) { $choiceLabels = $Choices | Select-Object -ExpandProperty $ChoiceLabelProperty } $duplicateLabels = $choiceLabels | Group-Object | Where-Object { $_.Count -gt 1 } if($duplicateLabels) { Write-Error "You have duplicate labels in your select list, this is ambiguous so a selection cannot be made" exit 2 } $prompt = [Spectre.Console.SelectionPromptExtensions]::AddChoices($prompt, [string[]]$choiceLabels) $prompt.Title = $Title $prompt.PageSize = $PageSize $prompt.WrapAround = $true $prompt.HighlightStyle = [Spectre.Console.Style]::new([Spectre.Console.Color]::$Color) $prompt.MoreChoicesText = "[$($script:DefaultValueColor)](Move up and down to reveal more choices)[/]" $selected = Invoke-SpectrePromptAsync -Prompt $prompt if($ChoiceLabelProperty) { $selected = $Choices | Where-Object -Property $ChoiceLabelProperty -Eq $selected } return $selected } function Read-SpectreMultiSelection { param ( [string] $Title = "What are your favourite [$($script:AccentColor.ToString())]colors[/]?", [array] $Choices = @("red", "orange", "yellow", "green", "blue", "indigo", "violet"), [string] $ChoiceLabelProperty, [ValidateScript({if($script:Colors -contains $_) { $true } else { throw "Color must be one of $($script:Colors -join ', ')" } })] [string] $Color = $script:AccentColor.ToString(), [int] $PageSize = 5 ) $prompt = [Spectre.Console.MultiSelectionPrompt[string]]::new() $choiceLabels = $Choices if($ChoiceLabelProperty) { $choiceLabels = $Choices | Select-Object -ExpandProperty $ChoiceLabelProperty } $duplicateLabels = $choiceLabels | Group-Object | Where-Object { $_.Count -gt 1 } if($duplicateLabels) { Write-Error "You have duplicate labels in your select list, this is ambiguous so a selection cannot be made" exit 2 } $prompt = [Spectre.Console.MultiSelectionPromptExtensions]::AddChoices($prompt, [string[]]$choiceLabels) $prompt.Title = $Title $prompt.PageSize = $PageSize $prompt.WrapAround = $true $prompt.HighlightStyle = [Spectre.Console.Style]::new([Spectre.Console.Color]::$Color) $prompt.InstructionsText = "[$($script:DefaultValueColor)](Press [$($script:AccentColor.ToString())]space[/] to toggle a choice and press [$($script:AccentColor.ToString())]<enter>[/] to submit your answer)[/]" $prompt.MoreChoicesText = "[$($script:DefaultValueColor)](Move up and down to reveal more choices)[/]" $selected = Invoke-SpectrePromptAsync -Prompt $prompt if($ChoiceLabelProperty) { $selected = $Choices | Where-Object -Property $ChoiceLabelProperty -Eq $selected } return $selected } function Read-SpectreMultiSelectionGrouped { param ( [string] $Title = "What are your favourite [$($script:AccentColor.ToString())]colors[/]?", [array] $Choices = @( @{ Name = "The rainbow" Choices = @("red", "orange", "yellow", "green", "blue", "indigo", "violet") }, @{ Name = "The other colors" Choices = @("black", "grey", "white") } ), [string] $ChoiceLabelProperty, [ValidateScript({if($script:Colors -contains $_) { $true } else { throw "Color must be one of $($script:Colors -join ', ')" } })] [string] $Color = $script:AccentColor.ToString(), [int] $PageSize = 10 ) $prompt = [Spectre.Console.MultiSelectionPrompt[string]]::new() $choiceLabels = $Choices.Choices if($ChoiceLabelProperty) { $choiceLabels = $Choices | Select-Object -ExpandProperty $ChoiceLabelProperty } $duplicateLabels = $choiceLabels | Group-Object | Where-Object { $_.Count -gt 1 } if($duplicateLabels) { Write-Error "You have duplicate labels in your select list, this is ambiguous so a selection cannot be made (even when using choice groups)" exit 2 } foreach($group in $Choices) { $choiceLabels = $group.Choices if($ChoiceLabelProperty) { $choiceLabels = $Choices | Select-Object -ExpandProperty $ChoiceLabelProperty } $prompt = [Spectre.Console.MultiSelectionPromptExtensions]::AddChoiceGroup($prompt, $group.Name, [string[]]$choiceLabels) } $prompt.Title = $Title $prompt.PageSize = $PageSize $prompt.WrapAround = $true $prompt.HighlightStyle = [Spectre.Console.Style]::new([Spectre.Console.Color]::$Color) $prompt.InstructionsText = "[$($script:DefaultValueColor)](Press [$($script:AccentColor.ToString())]space[/] to toggle a choice and press [$($script:AccentColor.ToString())]<enter>[/] to submit your answer)[/]" $prompt.MoreChoicesText = "[$($script:DefaultValueColor)](Move up and down to reveal more choices)[/]" $selected = Invoke-SpectrePromptAsync -Prompt $prompt if($ChoiceLabelProperty) { $selected = $Choices | Where-Object -Property $ChoiceLabelProperty -Eq $selected } return $selected } function Read-SpectreText { param ( [string] $Question = "What's your name?", [string] $DefaultAnswer = "Prefer not to say" ) $prompt = [Spectre.Console.TextPrompt[string]]::new($Question) $prompt.DefaultValueStyle = [Spectre.Console.Style]::new($script:DefaultValueColor) $prompt = [Spectre.Console.TextPromptExtensions]::DefaultValue($prompt, $DefaultAnswer) return Invoke-SpectrePromptAsync -Prompt $prompt } function Invoke-SpectreCommandWithStatus { param ( [scriptblock] $ScriptBlock, [string] $Spinner, [string] $Title, [ValidateScript({if($script:Colors -contains $_) { $true } else { throw "Color must be one of $($script:Colors -join ', ')" } })] [string] $Color = $script:AccentColor.ToString() ) [Spectre.Console.AnsiConsole]::Status().Start($Title, { param ( $ctx ) $ctx.Spinner = [Spectre.Console.Spinner+Known]::$Spinner $ctx.SpinnerStyle = [Spectre.Console.Style]::new([Spectre.Console.Color]::$Color) & $ScriptBlock }) } function Write-SpectreHost { param ( [string] $Message, [switch] $NoNewline ) if($NoNewline) { [Spectre.Console.AnsiConsole]::Markup($Message) } else { [Spectre.Console.AnsiConsole]::MarkupLine($Message) } } function Invoke-SpectreCommandWithProgress { param ( [scriptblock] $ScriptBlock ) [Spectre.Console.AnsiConsole]::Progress().Start({ param ( $ctx ) & $ScriptBlock $ctx }) } function Add-SpectreJob { param ( [object] $Context, [string] $JobName, [System.Management.Automation.Job] $Job ) return @{ Job = $Job Task = $Context.AddTask($JobName) } } # Adapted from https://key2consulting.com/powershell-how-to-display-job-progress/ function Wait-SpectreJobs { param ( [object] $Context, [array] $Jobs, [int] $TimeoutSeconds = 60 ) $timeout = (Get-Date).AddSeconds($TimeoutSeconds) while(!$Context.IsFinished) { if((Get-Date) -gt $timeout) { throw "Timed out waiting for jobs after $TimeoutSeconds seconds" } foreach($job in $Jobs) { $progress = 0.0 if($null -ne $job.Job.ChildJobs[0].Progress) { $progress = $job.Job.ChildJobs[0].Progress | Select-Object -Last 1 -ExpandProperty "PercentComplete" } $job.Task.Value = $progress } Start-Sleep -Milliseconds 100 } } function Format-SpectreBarChart { param ( [Parameter(ValueFromPipeline)] [array] $Data, $Title, $Width = $Host.UI.RawUI.Width ) begin { $barChart = [Spectre.Console.BarChart]::new() if($Title) { $barChart.Label = $Title } $barChart.Width = $Width } process { $barChart = [Spectre.Console.BarChartExtensions]::AddItem($barChart, $Data.Label, $Data.Value, $Data.Color) } end { [Spectre.Console.AnsiConsole]::Write($barChart) } } function Format-SpectreBreakdownChart { param ( [Parameter(ValueFromPipeline)] [array] $Data, $Width = $Host.UI.RawUI.Width ) begin { $chart = [Spectre.Console.BreakdownChart]::new() $chart.Width = $Width } process { $chart = [Spectre.Console.BreakdownChartExtensions]::AddItem($chart, $Data.Label, $Data.Value, $Data.Color) } end { [Spectre.Console.AnsiConsole]::Write($chart) } } function Format-SpectrePanel { param ( [Parameter(ValueFromPipeline)] [string] $Data, [string] $Title, [string] $Border = "Rounded", [switch] $Expand, [ValidateScript({if($script:Colors -contains $_) { $true } else { throw "Color must be one of $($script:Colors -join ', ')" } })] [string] $Color = $script:AccentColor.ToString() ) $panel = [Spectre.Console.Panel]::new($Data) if($Title) { $panel.Header = [Spectre.Console.PanelHeader]::new($Title) } $panel.Expand = $Expand $panel.Border = [Spectre.Console.BoxBorder]::$Border $panel.BorderStyle = [Spectre.Console.Style]::new([Spectre.Console.Color]::$Color) [Spectre.Console.AnsiConsole]::Write($panel) } function Format-SpectreTable { # TODO fix this to be not crap and use a formatter or something param ( [Parameter(ValueFromPipeline)] [array] $Data, [string] $Border = "Double", [ValidateScript({if($script:Colors -contains $_) { $true } else { throw "Color must be one of $($script:Colors -join ', ')" } })] [string] $Color = $script:AccentColor.ToString() ) begin { $table = [Spectre.Console.Table]::new() $table.Border = [Spectre.Console.TableBorder]::$Border $table.BorderStyle = [Spectre.Console.Style]::new([Spectre.Console.Color]::$Color) $headerProcessed = $false } process { if(!$headerProcessed) { $Data | Get-Member -MemberType Properties | Foreach-Object { $table.AddColumn($_.Name) | Out-Null } $headerProcessed = $true } $row = @() $Data | Get-Member -MemberType Properties | Foreach-Object { if($null -eq $Data."$($_.Name)") { $row += [Spectre.Console.Text]::new("") } else { $row += [Spectre.Console.Text]::new($Data."$($_.Name)".ToString()) } } $table = [Spectre.Console.TableExtensions]::AddRow($table, [Spectre.Console.Text[]]$row) } end { [Spectre.Console.AnsiConsole]::Write($table) } } function Format-SpectreTree { param ( [Parameter(ValueFromPipeline)] [hashtable] $Data, [string] $Border = "Rounded", [ValidateScript({if($script:Colors -contains $_) { $true } else { throw "Color must be one of $($script:Colors -join ', ')" } })] [string] $Color = $script:AccentColor.ToString() ) function Add-SpectreTreeNode { param ( $Node, $Children ) foreach($child in $Children) { $newNode = [Spectre.Console.HasTreeNodeExtensions]::AddNode($Node, $child.Label) if($child.Children.Count -gt 0) { Add-SpectreTreeNode -Node $newNode -Children $child.Children } } } $tree = [Spectre.Console.Tree]::new($Data.Label) Add-SpectreTreeNode -Node $tree -Children $Data.Children $tree.Style = [Spectre.Console.Style]::new([Spectre.Console.Color]::$Color) [Spectre.Console.AnsiConsole]::Write($tree) } function Read-SpectrePause { param ( [string] $Message = "[$script:DefaultValueColor]Press [$script:AccentColor]<enter>[/] to continue[/]", [switch] $NoNewline ) $position = $Host.UI.RawUI.CursorPosition if(!$NoNewline) { Write-Host "" } Write-SpectreHost $Message -NoNewline Read-Host $endPosition = $Host.UI.RawUI.CursorPosition if($endPosition -eq $position) { # Reached the end of the window [Console]::SetCursorPosition($position.X, $position.Y - 2) Write-Host (" " * $Message.Length) [Console]::SetCursorPosition($position.X, $position.Y - 2) } else { [Console]::SetCursorPosition($position.X, $position.Y) Write-Host (" " * $Message.Length) [Console]::SetCursorPosition($position.X, $position.Y) } } |