diff --git a/.github/workflows/build-test-publish.yml b/.github/workflows/build-test-publish.yml index 9819a3b3..e83817d5 100644 --- a/.github/workflows/build-test-publish.yml +++ b/.github/workflows/build-test-publish.yml @@ -1,11 +1,11 @@ name: Publish to PSGallery env: - PSGALLERY_API_KEY: ${{ secrets.PSGALLERY_API_KEY }} + PSGALLERY_API_KEY: ${{ secrets.PSGALLERY_API_KEY }} on: push: branches: - - main - - prerelease + - main + - prerelease permissions: contents: write @@ -26,7 +26,7 @@ jobs: $ErrorActionPreference = "Stop" & .\PwshSpectreConsole\Build.ps1 $env:PSModulePath = @($env:PSModulePath, ".\PwshSpectreConsole\") -join ":" - Invoke-Pester -CI + Invoke-Pester -CI -ExcludeTag "ExcludeCI" $version = Get-Module PwshSpectreConsole -ListAvailable | Sort-Object -Property Version -Descending | Select-Object -First 1 -ExpandProperty Version if($null -eq $version) { throw "Failed to load version" } $onlineVersion = Find-Module -Name PwshSpectreConsole -RequiredVersion $version -ErrorAction SilentlyContinue @@ -46,7 +46,7 @@ jobs: Import-Module .\PwshSpectreConsole\PwshSpectreConsole.psd1 -Force Publish-Module -Name PwshSpectreConsole -Exclude "Build.ps1" -NugetApiKey $env:PSGALLERY_API_KEY gh release create "v$newVersion" --target main --generate-notes - + publish-prerelease-to-psgallery: name: Publish Prerelease runs-on: ubuntu-latest @@ -62,11 +62,11 @@ jobs: $ErrorActionPreference = "Stop" & ./PwshSpectreConsole/Build.ps1 $env:PSModulePath = @($env:PSModulePath, ".\PwshSpectreConsole\") -join ":" - Invoke-Pester -CI + Invoke-Pester -CI -ExcludeTag "ExcludeCI" $version = Get-Module PwshSpectreConsole -ListAvailable | Sort-Object -Property Version -Descending | Select-Object -First 1 -ExpandProperty Version if($null -eq $version) { throw "Failed to load version" } $onlineVersions = Find-Module -Name PwshSpectreConsole -AllowPrerelease -AllVersions - + $latestStableVersion = $onlineVersions | Where-Object { $_.Version -notlike "*prerelease*" } | Sort-Object -Property Version -Descending | Select-Object -First 1 -ExpandProperty Version $latestStableVersion = [version]$latestStableVersion $latestPrereleaseVersion = $onlineVersions | Where-Object { $_.Version -like "*prerelease*" } | Sort-Object -Property Version -Descending | Select-Object -First 1 -ExpandProperty Version @@ -79,7 +79,7 @@ jobs: # Prerelease will always be at least one minor version above the latest published stable version so when it's merged to main the minor version will get bumped # To bump a major version the manifest would be edited manually to vnext.0.0 before merging to main $newVersion = [version]::new($latestStableVersion.Major, $latestStableVersion.Minor + 1, 0) - + if($newVersion -eq $oldVersion) { Write-Host "Version is not being bumped in prerelease" } else { @@ -100,4 +100,4 @@ jobs: Publish-Module -Name PwshSpectreConsole -Exclude "Build.ps1" -NugetApiKey $env:PSGALLERY_API_KEY -AllowPrerelease # Create a gh release for it - gh release create "v$newVersion-$newPrereleaseTag" --target prerelease --generate-notes --prerelease \ No newline at end of file + gh release create "v$newVersion-$newPrereleaseTag" --target prerelease --generate-notes --prerelease diff --git a/.github/workflows/unit-test-only.yml b/.github/workflows/unit-test-only.yml index 5fc73f39..9ab6aa66 100644 --- a/.github/workflows/unit-test-only.yml +++ b/.github/workflows/unit-test-only.yml @@ -19,5 +19,6 @@ jobs: $ErrorActionPreference = "Stop" & .\PwshSpectreConsole\Build.ps1 $env:PSModulePath = @($env:PSModulePath, ".\PwshSpectreConsole\") -join ":" - Invoke-Pester -CI - \ No newline at end of file + $PSVersionTable | Out-Host + Get-Module Pester -ListAvailable | Out-Host + Invoke-Pester -CI -ExcludeTag "ExcludeCI" diff --git a/PwshSpectreConsole.Tests/TestHelpers.psm1 b/PwshSpectreConsole.Tests/TestHelpers.psm1 index 7d0a3247..fdb361d7 100644 --- a/PwshSpectreConsole.Tests/TestHelpers.psm1 +++ b/PwshSpectreConsole.Tests/TestHelpers.psm1 @@ -19,7 +19,7 @@ function Get-RandomColor { } function Get-RandomList { - param ( + param( [int] $MinItems = 2, [int] $MaxItems = 10, [scriptblock] $Generator = { @@ -75,7 +75,7 @@ function Get-RandomChartItem { } function Get-RandomTree { - param ( + param( [hashtable] $Root, [int] $MinChildren = 1, [int] $MaxChildren = 3, @@ -105,7 +105,7 @@ function Get-RandomTree { $newTree = Get-RandomTree -Root $newChild -MaxChildren $MaxChildren -MaxDepth $MaxDepth -CurrentDepth $CurrentDepth $Root.Children += $newTree } - + return $Root } @@ -114,8 +114,102 @@ function Get-RandomBool { } function Get-RandomChoice { - param ( + param( [string[]] $Choices ) return $Choices[(Get-Random -Minimum 0 -Maximum $Choices.Count)] -} \ No newline at end of file +} + +function Get-SpectreRenderable { + param( + [Parameter(Mandatory)] + [Spectre.Console.Rendering.Renderable]$RenderableObject + ) + try { + $writer = [System.IO.StringWriter]::new() + $output = [Spectre.Console.AnsiConsoleOutput]::new($writer) + $settings = [Spectre.Console.AnsiConsoleSettings]::new() + $settings.Out = $output + $console = [Spectre.Console.AnsiConsole]::Create($settings) + $console.Write($RenderableObject) + $writer.ToString() + } + finally { + $writer.Dispose() + } +} + +function Get-AnsiEscapeSequence { + <# + could be useful for debugging + #> + param( + [Parameter(Mandatory, ValueFromPipeline)] + [String] $String + ) + process { + $Escaped = $String.EnumerateRunes() | ForEach-Object { + if ($_.Value -le 0x1f) { + [Text.Rune]::new($_.Value + 0x2400) + } else { + $_ + } + } | Join-String + [PSCustomObject]@{ + Escaped = $Escaped + Original = $String + Clean = [System.Management.Automation.Host.PSHostUserInterface]::GetOutputString($String, $false) + } + } +} + +function Get-PSStyleRandom { + param( + [Switch] $Foreground, + [Switch] $Background, + [Switch] $Decoration, + [Switch] $RGBForeground, + [Switch] $RGBBackground + ) + $Style = Switch ($PSBoundParameters.Keys) { + 'Foreground' { + $fg = ($PSStyle.Foreground | Get-Member -MemberType Property | Get-Random).Name + $PSStyle.Foreground.$fg + } + 'Background' { + $bg = ($PSStyle.Background | Get-Member -MemberType Property | Get-Random).Name + $PSStyle.Background.$bg + } + 'Decoration' { + $deco = ($PSStyle | Get-Member -MemberType Property | Where-Object { $_.Definition -match '^string' -And $_.Name -notmatch 'off$|Reset' } | Get-Random).Name + $PSStyle.$deco + } + 'RGBForeground' { + $r = Get-Random -min 0 -max 255 + $g = Get-Random -min 0 -max 255 + $b = Get-Random -min 0 -max 255 + $PSStyle.Foreground.FromRgb($r, $g, $b) + } + 'RGBBackground' { + $r = Get-Random -min 0 -max 255 + $g = Get-Random -min 0 -max 255 + $b = Get-Random -min 0 -max 255 + $PSStyle.Background.FromRgb($r, $g, $b) + } + } + return $Style | Join-String +} +Function Get-SpectreColorSample { + $spectreColors = [Spectre.Console.Color] | Get-Member -Static -Type Properties | Select-Object -ExpandProperty Name + foreach ($c in $spectreColors) { + $color = [Spectre.Console.Color]::$c + $renderable = [Spectre.Console.Text]::new("Hello, $c", [Spectre.Console.Style]::new($color)) + $SpectreString = Get-SpectreRenderable $renderable + [PSCustomObject]@{ + Color = $c + String = $SpectreString + # Object = $color + # Debug = Get-AnsiEscapeSequence $SpectreString + } + } +} diff --git a/PwshSpectreConsole.Tests/formatting/ConvertTo-SpectreDecoration.tests.ps1 b/PwshSpectreConsole.Tests/formatting/ConvertTo-SpectreDecoration.tests.ps1 new file mode 100644 index 00000000..4fbbf6a7 --- /dev/null +++ b/PwshSpectreConsole.Tests/formatting/ConvertTo-SpectreDecoration.tests.ps1 @@ -0,0 +1,41 @@ +Remove-Module PwshSpectreConsole -Force -ErrorAction SilentlyContinue +Import-Module "$PSScriptRoot\..\..\PwshSpectreConsole\PwshSpectreConsole.psd1" -Force +Import-Module "$PSScriptRoot\..\TestHelpers.psm1" -Force + +Describe "ConvertTo-SpectreDecoration" { + InModuleScope "PwshSpectreConsole" { + It "Test PSStyle Decorations" { + $PSStyleColor = Get-PSStyleRandom -Decoration + $reset = $PSStyle.Reset + $string = 'Hello, world!, hello universe!' + $sample = "{0}{1}{2}" -f $PSStyleColor, $string, $PSStyle.Reset + $test = Get-SpectreRenderable (ConvertTo-SpectreDecoration $sample) + $test | Should -Be $sample + } + It "Test PSStyle Foreground RGB Colors" -Tag "ExcludeCI" { + # testing something + $PSStyleColor = Get-PSStyleRandom -RGBForeground + $reset = $PSStyle.Reset + $string = 'Hello, world!, hello universe!' + $sample = "{0}{1}{2}" -f $PSStyleColor, $string, $PSStyle.Reset + $test = Get-SpectreRenderable (ConvertTo-SpectreDecoration $sample) + $test | Should -Be $sample + } + It "Test PSStyle Background RGB Colors" -Tag "ExcludeCI" { + $PSStyleColor = Get-PSStyleRandom -RGBBackground + $reset = $PSStyle.Reset + $string = 'Hello, world!, hello universe!' + $sample = "{0}{1}{2}" -f $PSStyleColor, $string, $PSStyle.Reset + $test = Get-SpectreRenderable (ConvertTo-SpectreDecoration $sample) + $test | Should -Be $sample + } + It "Test Spectre Colors" { + # this might work because the colors are generated from CI so shouldnt get us codes we cant render. + $sample = Get-SpectreColorSample + foreach ($item in $sample) { + $test = Get-SpectreRenderable (ConvertTo-SpectreDecoration $item.String) + $test | Should -Be $item.String + } + } + } +} diff --git a/PwshSpectreConsole.Tests/formatting/Format-SpectreTable.tests.ps1 b/PwshSpectreConsole.Tests/formatting/Format-SpectreTable.tests.ps1 index 7b9ae129..2095528a 100644 --- a/PwshSpectreConsole.Tests/formatting/Format-SpectreTable.tests.ps1 +++ b/PwshSpectreConsole.Tests/formatting/Format-SpectreTable.tests.ps1 @@ -6,24 +6,14 @@ Describe "Format-SpectreTable" { InModuleScope "PwshSpectreConsole" { BeforeEach { $testData = $null - $testBorder = "None" #Get-RandomBoxBorder + $testBorder = Get-RandomBoxBorder $testColor = Get-RandomColor - Mock Write-AnsiConsole { - if($RenderableObject -isnot [Spectre.Console.Table]) { - throw "Found $($RenderableObject.GetType().Name), expected [Spectre.Console.Table]" - } - $borderType = ($testBorder -eq "None") ? "NoTableBorder" : $testBorder - if($RenderableObject.Border.GetType().Name -notlike "*$borderType*") { - throw "Found $($RenderableObject.Border.GetType().Name), expected border like *$borderType*" - } - if($RenderableObject.BorderStyle.Foreground.ToMarkup() -ne $testColor) { - throw "Found $($RenderableObject.BorderStyle.Foreground.ToMarkup()), expected $testColor" - } - if($RenderableObject.Rows.Count -ne $testData.Count) { - throw "Found $($RenderableObject.Rows.Count), expected $($testData.Count)" - } - Write-Debug "Input data was $($RenderableObject.Rows.Count) rows, $($RenderableObject.Columns.Count) columns, border $($RenderableObject.BorderStyle.Foreground.ToMarkup()), borderstyle, $($RenderableObject.BorderStyle.GetType().Name)" + Mock Write-AnsiConsole -Verifiable -ParameterFilter { + $RenderableObject -is [Spectre.Console.Table] ` + -and ($testBorder -eq "None" -or $RenderableObject.Border.GetType().Name -like "*$testBorder*") ` + -and $RenderableObject.BorderStyle.Foreground.ToMarkup() -eq $testColor ` + -and $RenderableObject.Rows.Count -eq $testData.Count } } @@ -33,5 +23,107 @@ Describe "Format-SpectreTable" { Assert-MockCalled -CommandName "Write-AnsiConsole" -Times 1 -Exactly Should -InvokeVerifiable } + + It "Should create a table when default display members for a command are required and input is piped" { + $testData = Get-ChildItem "$PSScriptRoot" + $testData | Format-SpectreTable -Border $testBorder -Color $testColor + Assert-MockCalled -CommandName "Write-AnsiConsole" -Times 1 -Exactly + Should -InvokeVerifiable + } + + It "Should be able to retrieve default display members for command output with format data" { + $testData = Get-ChildItem "$PSScriptRoot" + $defaultDisplayMembers = $testData | Get-DefaultDisplayMembers + if($IsLinux -or $IsMacOS) { + # Expected @('UnixMode', 'User', 'Group', 'LastWrite…', 'Size', 'Name'), but got @('UnixMode', 'User', 'Group', 'LastWriteTime', 'Size', 'Name'). + # i have no idea whats truncating LastWriteTime + # $defaultDisplayMembers.Properties.GetEnumerator().Name | Should -Be @("UnixMode", "User", "Group", "LastWriteTime", "Size", "Name") + $defaultDisplayMembers.Properties.GetEnumerator().Name | Should -Match 'UnixMode|User|Group|LastWrite|Size|Name' + } else { + $defaultDisplayMembers.Properties.GetEnumerator().Name | Should -Be @("Mode", "LastWriteTime", "Length", "Name") + } + } + + It "Should not throw and should return null when input does not have format data" { + { + $defaultDisplayMembers = [hashtable]@{ + "Hello" = "World" + } | Get-DefaultDisplayMembers + $defaultDisplayMembers | Should -Be $null + } | Should -Not -Throw + } + + It "Should be able to format ansi strings" { + $rawString = "hello world" + $ansiString = "`e[31mhello `e[46mworld`e[0m" + $result = ConvertTo-SpectreDecoration -String $ansiString + $result.Length | Should -Be $rawString.Length + } + + It "Should be able to format PSStyle strings" { + $rawString = "" + $ansiString = "" + $PSStyle | Get-Member -MemberType Property | Where-Object { $_.Definition -match '^string' -And $_.Name -notmatch 'off$|Reset' } | ForEach-Object { + $name = $_.Name + $rawString += "$name " + $ansiString += "$($PSStyle.$name)$name " + } + $ansiString += "$($PSStyle.Reset)" + $result = ConvertTo-SpectreDecoration -String $ansiString + $result.Length | Should -Be $rawString.Length + } + + It "Should be able to format strings with spectre markup when opted in" { + $rawString = "hello spectremarkup world" + $ansiString = "hello [red]spectremarkup[/] world" + $result = ConvertTo-SpectreDecoration -String $ansiString -AllowMarkup + $result.Length | Should -Be $rawString.Length + } + + It "Should leave spectre markup alone by default" { + $ansiString = "hello [red]spectremarkup[/] world" + $result = ConvertTo-SpectreDecoration -String $ansiString + $result.Length | Should -Be $ansiString.Length + } + + It "Should be able to create a new table cell with spectre markup" { + $rawString = "hello spectremarkup world" + $ansiString = "hello [red]spectremarkup[/] world" + $result = New-TableCell -String $ansiString -AllowMarkup + $result | Should -BeOfType [Spectre.Console.Markup] + $result.Length | Should -Be $rawString.Length + } + + It "Should be able to create a new table cell without spectre markup by default" { + $ansiString = "hello [red]spectremarkup[/] world" + $result = New-TableCell -String $ansiString + $result | Should -BeOfType [Spectre.Console.Text] + $result.Length | Should -Be $ansiString.Length + } + + It "Should be able to create a new table row with spectre markup" { + $rawString = "Markup" + $entryItem = [pscustomobject]@{ + "Markup" = "[red]Markup[/]" + "Also" = "Hello" + } + $result = New-TableRow -Entry $entryItem -AllowMarkup + $result -is [array] | Should -Be $true + $result[0] | Should -BeOfType [Spectre.Console.Markup] + $result[0].Length | Should -Be $rawString.Length + $result.Count | Should -Be ($entryItem.PSObject.Properties | Measure-Object).Count + } + + It "Should be able to create a new table row without spectre markup by default" { + $entryItem = [pscustomobject]@{ + "Markup" = "[red]Markup[/]" + "Also" = "Hello" + } + $result = New-TableRow -Entry $entryItem + $result -is [array] | Should -Be $true + $result[0] | Should -BeOfType [Spectre.Console.Text] + $result[0].Length | Should -Be $entryItem.Markup.Length + $result.Count | Should -Be ($entryItem.PSObject.Properties | Measure-Object).Count + } } -} \ No newline at end of file +} diff --git a/PwshSpectreConsole.Tests/formatting/new_Format-SpectreTable.tests.ps1 b/PwshSpectreConsole.Tests/formatting/new_Format-SpectreTable.tests.ps1 new file mode 100644 index 00000000..5a9e99b3 --- /dev/null +++ b/PwshSpectreConsole.Tests/formatting/new_Format-SpectreTable.tests.ps1 @@ -0,0 +1,52 @@ +Remove-Module PwshSpectreConsole -Force -ErrorAction SilentlyContinue +Import-Module "$PSScriptRoot\..\..\PwshSpectreConsole\PwshSpectreConsole.psd1" -Force +Import-Module "$PSScriptRoot\..\TestHelpers.psm1" -Force + +Describe "Format-SpectreTable" { + InModuleScope "PwshSpectreConsole" { + BeforeEach { + $testData = $null + $testBorder = 'Markdown' + $testColor = Get-RandomColor + Mock Write-AnsiConsole { + param( + [Parameter(Mandatory)] + [Spectre.Console.Rendering.Renderable] $RenderableObject + ) + try { + $writer = [System.IO.StringWriter]::new() + $output = [Spectre.Console.AnsiConsoleOutput]::new($writer) + $settings = [Spectre.Console.AnsiConsoleSettings]::new() + $settings.Out = $output + $console = [Spectre.Console.AnsiConsole]::Create($settings) + $console.Write($RenderableObject) + $writer.ToString() + } + finally { + $writer.Dispose() + } + } + } + It "Should create a table when default display members for a command are required" { + $testData = Get-ChildItem "$PSScriptRoot" + $verification = Get-DefaultDisplayMembers $testData + $testResult = Format-SpectreTable -Data $testData -Border $testBorder -Color $testColor + $command = Get-Command "Select-Object" + $rows = $testResult -split "\r?\n" | Select-Object -Skip 1 | Select-Object -SkipLast 2 + $header = $rows[0] + $properties = $header -split '\|' | Get-AnsiEscapeSequence | ForEach-Object { + if (-Not [String]::IsNullOrWhiteSpace($_.Clean)) { + $_.Clean -replace '\s+' + } + } + if ($IsLinux -or $IsMacOS) { + $verification.Properties.keys | Should -Match 'UnixMode|User|Group|LastWrite|Size|Name' + } + else { + $verification.Properties.keys | Should -Be $properties + } + Assert-MockCalled -CommandName "Write-AnsiConsole" -Times 1 -Exactly + Should -InvokeVerifiable + } + } +} diff --git a/PwshSpectreConsole.Tests/writing/Write-SpectreCalender.tests.ps1 b/PwshSpectreConsole.Tests/writing/Write-SpectreCalender.tests.ps1 new file mode 100644 index 00000000..81d88c2a --- /dev/null +++ b/PwshSpectreConsole.Tests/writing/Write-SpectreCalender.tests.ps1 @@ -0,0 +1,72 @@ +Remove-Module PwshSpectreConsole -Force -ErrorAction SilentlyContinue +Import-Module "$PSScriptRoot\..\..\PwshSpectreConsole\PwshSpectreConsole.psd1" -Force +Import-Module "$PSScriptRoot\..\TestHelpers.psm1" -Force + +Describe "Write-SpectreCalendar" { + InModuleScope "PwshSpectreConsole" { + BeforeEach { + $testBorder = 'Markdown' + $testColor = Get-RandomColor + Mock Write-AnsiConsole { + param( + [Parameter(Mandatory)] + [Spectre.Console.Rendering.Renderable] $RenderableObject + ) + try { + $writer = [System.IO.StringWriter]::new() + $output = [Spectre.Console.AnsiConsoleOutput]::new($writer) + $settings = [Spectre.Console.AnsiConsoleSettings]::new() + $settings.Out = $output + $console = [Spectre.Console.AnsiConsole]::Create($settings) + $console.Write($RenderableObject) + $writer.ToString() + } + finally { + $writer.Dispose() + } + } + } + + It "writes calendar for a date" { + $sample = Write-SpectreCalendar -Date "2024-01-01" -Culture "en-us" -Border $testBorder -Color $testColor + $object = $sample -split '\r?\n' + $object[0] | Should -Match 'January\s+2024' + $rawdays = $object[2] + $days = $rawdays -split '\|' | Get-AnsiEscapeSequence | ForEach-Object { + if (-Not [String]::IsNullOrWhiteSpace($_.Clean)) { + $_.Clean -replace '\s+' + } + } + $answer = (Get-Culture -Name en-us).DateTimeFormat.AbbreviatedDayNames + # $days | Should -Be @('Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat') + $days | Should -Be $answer + Assert-MockCalled -CommandName "Write-AnsiConsole" -Times 1 -Exactly + } + + It "writes calendar for a date with events" { + $events = @{ + '2022-03-10' = 'Event 1' + '2022-03-20' = 'Event 2' + } + $sample = Write-SpectreCalendar -Date "2024-03-01" -Events $events -Culture "en-us" -Border Markdown -Color $testColor + $sample.count | Should -Be 2 + $sample[0] | Should -Match 'March\s+2024' + $sample[1] | Should -Match 'Event 1' + Assert-MockCalled -CommandName "Write-AnsiConsole" -Times 2 -Exactly + } + It "writes calendar for a date with events" { + $sample = Write-SpectreCalendar -Date 2024-07-01 -HideHeader -Border Markdown -Color $testColor + $object = $sample -split '\r?\n' | Select-Object -Skip 1 | Select-Object -SkipLast 3 + $object.count | Should -Be 7 + [string[]]$results = 1..31 + $object | Select-Object -Skip 2 | ForEach-Object { + $_ -split '\|' | Get-AnsiEscapeSequence | ForEach-Object { + if (-Not [String]::IsNullOrWhiteSpace($_.Clean)) { + $_.Clean -replace '\s+' | Should -BeIn $results + } + } + } + Assert-MockCalled -CommandName "Write-AnsiConsole" -Times 1 -Exactly + } + } +} diff --git a/PwshSpectreConsole/Build.ps1 b/PwshSpectreConsole/Build.ps1 index 70ac1623..9daab3be 100644 --- a/PwshSpectreConsole/Build.ps1 +++ b/PwshSpectreConsole/Build.ps1 @@ -1,5 +1,5 @@ param ( - [string] $Version = "0.47.0" + [string] $Version = "0.48.0" ) function Install-SpectreConsole { @@ -48,4 +48,4 @@ $installLocation = (Join-Path $PSScriptRoot "packages") if(Test-Path $installLocation) { Remove-Item $installLocation -Recurse -Force } -Install-SpectreConsole -InstallLocation $installLocation -Version $Version \ No newline at end of file +Install-SpectreConsole -InstallLocation $installLocation -Version $Version diff --git a/PwshSpectreConsole/PwshSpectreConsole.psd1 b/PwshSpectreConsole/PwshSpectreConsole.psd1 index e249115d..17ed4b9e 100644 --- a/PwshSpectreConsole/PwshSpectreConsole.psd1 +++ b/PwshSpectreConsole/PwshSpectreConsole.psd1 @@ -54,10 +54,10 @@ PowerShellVersion = '7.0' # RequiredModules = @() # Assemblies that must be loaded prior to importing this module -RequiredAssemblies = - '.\packages\Spectre.Console\lib\netstandard2.0\Spectre.Console.dll', - '.\packages\Spectre.Console.ImageSharp\lib\netstandard2.0\Spectre.Console.ImageSharp.dll', - '.\packages\SixLabors.ImageSharp\lib\netstandard2.0\SixLabors.ImageSharp.dll', +RequiredAssemblies = + '.\packages\Spectre.Console\lib\netstandard2.0\Spectre.Console.dll', + '.\packages\Spectre.Console.ImageSharp\lib\netstandard2.0\Spectre.Console.ImageSharp.dll', + '.\packages\SixLabors.ImageSharp\lib\netstandard2.0\SixLabors.ImageSharp.dll', '.\packages\Spectre.Console.Json\lib\netstandard2.0\Spectre.Console.Json.dll' # Script files (.ps1) that are run in the caller's environment prior to importing this module. @@ -73,18 +73,19 @@ RequiredAssemblies = # NestedModules = @() # Functions 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 functions to export. -FunctionsToExport = 'Add-SpectreJob', 'Format-SpectreBarChart', - 'Format-SpectreBreakdownChart', 'Format-SpectrePanel', - 'Format-SpectreTable', 'Format-SpectreTree', 'Get-SpectreEscapedText', - 'Get-SpectreImage', 'Get-SpectreImageExperimental', - 'Invoke-SpectreCommandWithProgress', - 'Invoke-SpectreCommandWithStatus', 'Read-SpectreMultiSelection', - 'Read-SpectreMultiSelectionGrouped', 'Read-SpectrePause', - 'Read-SpectreSelection', 'Read-SpectreText', 'Set-SpectreColors', - 'Start-SpectreDemo', 'Wait-SpectreJobs', 'Write-SpectreFigletText', - 'Write-SpectreHost', 'Write-SpectreRule', 'Read-SpectreConfirm', - 'New-SpectreChartItem', 'Invoke-SpectreScriptBlockQuietly', - 'Get-SpectreDemoColors', 'Get-SpectreDemoEmoji', 'Format-SpectreJson' +FunctionsToExport = 'Add-SpectreJob', 'Format-SpectreBarChart', + 'Format-SpectreBreakdownChart', 'Format-SpectrePanel', + 'Format-SpectreTable', 'Format-SpectreTree', 'Get-SpectreEscapedText', + 'Get-SpectreImage', 'Get-SpectreImageExperimental', + 'Invoke-SpectreCommandWithProgress', + 'Invoke-SpectreCommandWithStatus', 'Read-SpectreMultiSelection', + 'Read-SpectreMultiSelectionGrouped', 'Read-SpectrePause', + 'Read-SpectreSelection', 'Read-SpectreText', 'Set-SpectreColors', + 'Start-SpectreDemo', 'Wait-SpectreJobs', 'Write-SpectreFigletText', + 'Write-SpectreHost', 'Write-SpectreRule', 'Read-SpectreConfirm', + 'New-SpectreChartItem', 'Invoke-SpectreScriptBlockQuietly', + 'Get-SpectreDemoColors', 'Get-SpectreDemoEmoji', 'Format-SpectreJson', + 'Write-SpectreCalendar' # 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 = @() @@ -144,4 +145,3 @@ PrivateData = @{ # DefaultCommandPrefix = '' } - diff --git a/PwshSpectreConsole/private/Add-TableColumns.ps1 b/PwshSpectreConsole/private/Add-TableColumns.ps1 new file mode 100644 index 00000000..912681e2 --- /dev/null +++ b/PwshSpectreConsole/private/Add-TableColumns.ps1 @@ -0,0 +1,56 @@ +using namespace Spectre.Console + +function Add-TableColumns { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + $table, + [Parameter(Mandatory)] + $Object, + [Collections.Specialized.OrderedDictionary] + $FormatData, + [String[]] + $Property, + [String] + $Title + ) + Write-Debug "Module: $($ExecutionContext.SessionState.Module.Name) Command: $($MyInvocation.MyCommand.Name) Param: $($PSBoundParameters.GetEnumerator())" + if ($Property) { + Write-Debug 'Adding column from property' + foreach ($prop in $Property) { + $table.AddColumn($prop) | Out-Null + } + } elseif ($FormatData) { + foreach ($key in $FormatData.keys) { + $lookup = $FormatData[$key] + Write-Debug "Adding column from formatdata: $($lookup.GetEnumerator())" + $table.AddColumn($lookup.Label) | Out-Null + $table.Columns[-1].Padding = [Spectre.Console.Padding]::new(1, 0, 1, 0) + if ($lookup.width -gt 0) { + # width 0 is autosize, select the last entry in the column list + $table.Columns[-1].Width = $lookup.Width + } + if ($lookup.Alignment -ne 'undefined') { + $table.Columns[-1].Alignment = [Justify]::$lookup.Alignment + } + } + } elseif (Test-IsScalar $Object) { + # simple/scalar types show up wonky, we can detect them and just use a dummy header for the table + Write-Debug 'simple/scalar type' + $script:scalarDetected = $true + if ($Title) { + $table.AddColumn($Title) | Out-Null + } else { + $table.AddColumn("Value") | Out-Null + } + } else { + # no formatting found and no properties selected, enumerating psobject.properties.name + Write-Debug 'PSCustomObject/Properties switch detected' + foreach ($prop in $Object.psobject.Properties.Name) { + if (-Not [String]::IsNullOrEmpty($prop)) { + $table.AddColumn($prop) | Out-Null + } + } + } + return $table +} diff --git a/PwshSpectreConsole/private/ConvertFrom-ConsoleColor.ps1 b/PwshSpectreConsole/private/ConvertFrom-ConsoleColor.ps1 new file mode 100644 index 00000000..876e6dbb --- /dev/null +++ b/PwshSpectreConsole/private/ConvertFrom-ConsoleColor.ps1 @@ -0,0 +1,41 @@ +function ConvertFrom-ConsoleColor { + param( + [int]$Color + ) + Write-Debug "Module: $($ExecutionContext.SessionState.Module.Name) Command: $($MyInvocation.MyCommand.Name) Param: $($PSBoundParameters.GetEnumerator())" + $consoleColors = @{ + 30 = 'Black' + 31 = 'DarkRed' + 32 = 'DarkGreen' + 33 = 'DarkYellow' + 34 = 'DarkBlue' + 35 = 'DarkMagenta' + 36 = 'DarkCyan' + 37 = 'Gray' + 40 = 'Black' + 41 = 'DarkRed' + 42 = 'DarkGreen' + 43 = 'DarkYellow' + 44 = 'DarkBlue' + 45 = 'DarkMagenta' + 46 = 'DarkCyan' + 47 = 'Gray' + 90 = 'DarkGray' + 91 = 'Red' + 92 = 'Green' + 93 = 'Yellow' + 94 = 'Blue' + 95 = 'Magenta' + 96 = 'Cyan' + 97 = 'White' + 100 = 'DarkGray' + 101 = 'Red' + 102 = 'Green' + 103 = 'Yellow' + 104 = 'Blue' + 105 = 'Magenta' + 106 = 'Cyan' + 107 = 'White' + } + return $consoleColors[$Color] +} diff --git a/PwshSpectreConsole/private/ConvertTo-SpectreDecoration.ps1 b/PwshSpectreConsole/private/ConvertTo-SpectreDecoration.ps1 index cf7dc739..12ab3047 100644 --- a/PwshSpectreConsole/private/ConvertTo-SpectreDecoration.ps1 +++ b/PwshSpectreConsole/private/ConvertTo-SpectreDecoration.ps1 @@ -4,14 +4,19 @@ function ConvertTo-SpectreDecoration { [String]$String, [switch]$AllowMarkup ) + Write-Debug "Module: $($ExecutionContext.SessionState.Module.Name) Command: $($MyInvocation.MyCommand.Name) Param: $($PSBoundParameters.GetEnumerator())" if (-Not ('PwshSpectreConsole.VTCodes.Parser' -as [type])) { Add-PwshSpectreConsole.VTCodes } - Write-Debug "ANSI String: $String '$($String -replace '\x1B','e')'" $lookup = [PwshSpectreConsole.VTCodes.Parser]::Parse($String) - $ht = @{} + $ht = @{ + decoration = [Spectre.Console.Decoration]::None + fg = [Spectre.Console.Color]::Default + bg = [Spectre.Console.Color]::Default + } foreach ($item in $lookup) { - if ($item.value -eq 'reset') { + # Write-Debug "Type: $($item.type) Value: $($item.value) Position: $($item.position) Color: $($item.color)" + if ($item.value -eq 'None') { continue } $conversion = switch ($item.type) { @@ -20,7 +25,10 @@ function ConvertTo-SpectreDecoration { [Spectre.Console.Color]::FromConsoleColor($item.value) } else { - [Spectre.Console.Color]::FromInt32($item.value) + # spectre doesn't appear to have a way to convert from 4bit. + # e.g all $PSStyle colors 30-37, 40-47 and 90-97, 100-107 + # this will return the closest color in 8bit. + [Spectre.Console.Color]::FromConsoleColor((ConvertFrom-ConsoleColor $item.value)) } } '8bit' { @@ -43,10 +51,10 @@ function ConvertTo-SpectreDecoration { $ht.bg = $conversion } } - $String = $String -replace '\x1B\[[0-?]*[ -/]*[@-~]' + $String = [System.Management.Automation.Host.PSHostUserInterface]::GetOutputString($String, $false) Write-Debug "Clean: '$String' deco: '$($ht.decoration)' fg: '$($ht.fg)' bg: '$($ht.bg)'" - if($AllowMarkup) { - return [Spectre.Console.Markup]::new($String,[Spectre.Console.Style]::new($ht.fg,$ht.bg,$ht.decoration)) + if ($AllowMarkup) { + return [Spectre.Console.Markup]::new($String, [Spectre.Console.Style]::new($ht.fg, $ht.bg, $ht.decoration)) } - [Spectre.Console.Text]::new($String,[Spectre.Console.Style]::new($ht.fg,$ht.bg,$ht.decoration)) + return [Spectre.Console.Text]::new($String, [Spectre.Console.Style]::new($ht.fg, $ht.bg, $ht.decoration)) } diff --git a/PwshSpectreConsole/private/Get-DefaultDisplayMembers.ps1 b/PwshSpectreConsole/private/Get-DefaultDisplayMembers.ps1 index 45f011e7..a3e8307f 100644 --- a/PwshSpectreConsole/private/Get-DefaultDisplayMembers.ps1 +++ b/PwshSpectreConsole/private/Get-DefaultDisplayMembers.ps1 @@ -17,55 +17,37 @@ Write-Debug "getting formatdata for $($Object[0].PSTypeNames)" $formatData = Get-FormatData -TypeName $Object[0].PSTypeNames | Select-Object -First 1 Write-Debug "formatData: $($formatData.count)" + } catch { + # error getting formatdata, return null + return $null } - catch { - # no formatdata found + if (-Not $formatData) { + # no formatdata, return null return $null } - if ($formatData) { - $properties = [ordered]@{} - $labels = @{} - # $regex = [regex]::New('(?x)\$_\.(?[^\s,]+)') - $viewDefinition = $formatData.FormatViewDefinition | Where-Object { $_.Control -match 'TableControl' } | Select-Object -First 1 - Write-Debug "viewDefinition: $($viewDefinition.Name)" - $format = for ($i = 0; $i -lt $viewDefinition.Control.Headers.Count; $i++) { - $name = $viewDefinition.Control.Headers[$i].Label - $displayEntry = $viewDefinition.Control.Rows.Columns[$i].DisplayEntry - if (-not $name) { - $name = $displayEntry.Value - } - if ($labels.ContainsKey($name)) { - Write-Debug 'duplicate label found' - # im not sure why this is needed, but for filesystem we get both 'Mode' and 'ModeWithoutHardLink' with "label" Mode. - continue - } - $labels[$name] = $true - switch ($displayEntry.ValueType) { - 'Property' { - $expression = $displayEntry.Value - # $property = $displayEntry.Value - } - 'ScriptBlock' { - $expression = [ScriptBlock]::Create($displayEntry.Value) - # $property = $regex.matches($displayEntry.Value).foreach({ $_.Groups['Property'].Value }) | Select-Object -Unique - } - } - $properties[$name] = @{ - Label = $name - Width = $viewDefinition.Control.headers[$i].width - Alignment = $viewDefinition.Control.headers[$i].alignment - # Property = $property - # Expression = $expression - # PropertyType = $Object.PSObject.Properties[$property].TypeNameOfValue - # Type = $displayEntry.ValueType - } - @{ Name = $name; Expression = $expression } + # this needs to ordered to preserve table column order. + $properties = [ordered]@{} + $viewDefinition = $formatData.FormatViewDefinition | Where-Object { $_.Control -match 'TableControl' } | Select-Object -First 1 + Write-Debug "viewDefinition: $($viewDefinition.Name)" + $format = for ($i = 0; $i -lt $viewDefinition.Control.Headers.Count; $i++) { + $name = $viewDefinition.Control.Headers[$i].Label + $displayEntry = $viewDefinition.Control.Rows.Columns[$i].DisplayEntry + if (-not $name) { + $name = $displayEntry.Value + } + $expression = switch ($displayEntry.ValueType) { + 'Property' { $displayEntry.Value } + 'ScriptBlock' { [ScriptBlock]::Create($displayEntry.Value) } } - # we still need the properties to create the columns, but this function can be simplified. - # temporarily leaving it commented out for testing. - return [PSCustomObject]@{ - Properties = $properties - Format = $format + $properties[$name] = @{ + Label = $name + Width = $viewDefinition.Control.headers[$i].width + Alignment = $viewDefinition.Control.headers[$i].alignment } + @{ Name = $name; Expression = $expression } + } + return [PSCustomObject]@{ + Properties = $properties + Format = $format } } diff --git a/PwshSpectreConsole/private/Get-SpectreProfile.ps1 b/PwshSpectreConsole/private/Get-SpectreProfile.ps1 new file mode 100644 index 00000000..a79837b4 --- /dev/null +++ b/PwshSpectreConsole/private/Get-SpectreProfile.ps1 @@ -0,0 +1,22 @@ +function Get-SpectreProfile { + [CmdletBinding()] + param () + $object = [Spectre.Console.AnsiConsole]::Profile + return [PSCustomObject]@{ + Enrichers = $object.Enrichers -join ', ' + ColorSystem = $object.Capabilities.ColorSystem + Unicode = $object.Capabilities.Unicode + Ansi = $object.Capabilities.Ansi + Links = $object.Capabilities.Links + Legacy = $object.Capabilities.Legacy + Interactive = $object.Capabilities.Interactive + Terminal = $object.out.IsTerminal + Writer = $object.Out.Writer + Width = $object.Width + Height = $object.Height + Encoding = $object.Encoding.EncodingName + PSStyle = $PSStyle.OutputRendering + ConsoleOutputEncoding = [console]::OutputEncoding + ConsoleInputEncoding = [console]::InputEncoding + } +} diff --git a/PwshSpectreConsole/private/New-TableCell.ps1 b/PwshSpectreConsole/private/New-TableCell.ps1 new file mode 100644 index 00000000..08165f7b --- /dev/null +++ b/PwshSpectreConsole/private/New-TableCell.ps1 @@ -0,0 +1,29 @@ +function New-TableCell { + [cmdletbinding()] + param( + $String, + [Switch]$AllowMarkup + ) + Write-Debug "Module: $($ExecutionContext.SessionState.Module.Name) Command: $($MyInvocation.MyCommand.Name) Param: $($PSBoundParameters.GetEnumerator())" + if ([String]::IsNullOrEmpty($String)) { + if ($AllowMarkup) { + return [Spectre.Console.Markup]::new(' ') + } + return [Spectre.Console.Text]::new(' ') + } + if (-Not [String]::IsNullOrEmpty($String.ToString())) { + if ($AllowMarkup) { + Write-Debug "New-TableCell ToString(), Markup, $($String.ToString())" + return [Spectre.Console.Markup]::new($String.ToString()) + } + Write-Debug "New-TableCell ToString(), Text, $($String.ToString())" + return [Spectre.Console.Text]::new($String.ToString()) + } + # just coerce to string. + if ($AllowMarkup) { + Write-Debug "New-TableCell [String], markup, $([String]$String)" + return [Spectre.Console.Markup]::new([String]$String) + } + Write-Debug "New-TableCell [String], Text, $([String]$String)" + return [Spectre.Console.Text]::new([String]$String) +} diff --git a/PwshSpectreConsole/private/New-TableRow.ps1 b/PwshSpectreConsole/private/New-TableRow.ps1 new file mode 100644 index 00000000..ce6514f5 --- /dev/null +++ b/PwshSpectreConsole/private/New-TableRow.ps1 @@ -0,0 +1,47 @@ +function New-TableRow { + param( + $Entry, + [Switch] $FormatFound, + [Switch] $PropertiesSelected, + [Switch] $AllowMarkup + ) + Write-Debug "Module: $($ExecutionContext.SessionState.Module.Name) Command: $($MyInvocation.MyCommand.Name) Param: $($PSBoundParameters.GetEnumerator())" + $opts = @{} + if ($AllowMarkup) { + $opts.AllowMarkup = $true + } + if ((-Not $FormatFound -or -Not $PropertiesSelected) -And ($scalarDetected -eq $true)) { + New-TableCell -String $Entry @opts + } + else { + # simplified, should be faster. + $detectVT = '\x1b' + $rows = foreach ($cell in $Entry.psobject.Properties) { + if ([String]::IsNullOrEmpty($cell.Value)) { + New-TableCell @opts + continue + } + if ($cell.value -match $detectVT) { + if ($FormatFound) { + # we are dealing with an object that has VT codes and a formatdata entry. + # this returns a spectre.console.text/markup object with the VT codes applied. + ConvertTo-SpectreDecoration -String $cell.Value @opts + continue + } + else { + # we are dealing with an object that has VT codes but no formatdata entry. + # this returns a string with the VT codes stripped. + # we could pass it to ConvertTo-SpectreDecoration, should we? + # note if multiple colors are used it will only use the last color. + # better to use Markup to manually add colors. + Write-Debug "VT codes detected, but no formatdata entry. stripping VT codes, preferred method of manually adding colors is markup" + New-TableCell -String ([System.Management.Automation.Host.PSHostUserInterface]::GetOutputString($cell.Value, $false)) @opts + # ConvertTo-SpectreDecoration -String $cell.Value @opts + continue + } + } + New-TableCell -String $cell.Value @opts + } + return $rows + } +} diff --git a/PwshSpectreConsole/private/Test-IsScalar.ps1 b/PwshSpectreConsole/private/Test-IsScalar.ps1 new file mode 100644 index 00000000..920eee90 --- /dev/null +++ b/PwshSpectreConsole/private/Test-IsScalar.ps1 @@ -0,0 +1,13 @@ +function Test-IsScalar { + [CmdletBinding()] + param ( + $Value + ) + Write-Debug "Module: $($ExecutionContext.SessionState.Module.Name) Command: $($MyInvocation.MyCommand.Name) Param: $($PSBoundParameters.GetEnumerator())" + if ($Value -is [System.Collections.IEnumerable] -and $Value -isnot [string]) { + $firstItem = $Value | Select-Object -First 1 + return $firstItem -is [System.ValueType] -or $firstItem -is [System.String] + } else { + return $Value -is [System.ValueType] -or $Value -is [System.String] + } +} diff --git a/PwshSpectreConsole/private/classes/PwshSpectreConsole.VTCodes.cs b/PwshSpectreConsole/private/classes/PwshSpectreConsole.VTCodes.cs index 2da4c11e..40b30487 100644 --- a/PwshSpectreConsole/private/classes/PwshSpectreConsole.VTCodes.cs +++ b/PwshSpectreConsole/private/classes/PwshSpectreConsole.VTCodes.cs @@ -40,27 +40,25 @@ public static bool TryGetValue(int key, out string value) } internal static Dictionary DecorationDict { get; } = new Dictionary() { - { 0, "reset" }, - { 1, "bold" }, - { 2, "faint" }, - { 3, "italic" }, - { 4, "underline" }, - { 5, "blinkSlow" }, - { 6, "blinkRapid" }, - { 7, "reverseVideo" }, - { 8, "conceal" }, - { 9, "crossedOut" }, - { 21, "boldOff" }, - { 22, "normalIntensity" }, - { 23, "italicOff" }, - { 24, "underlineOff" }, - { 25, "blinkOff" }, - { 27, "inverseOff" }, - { 28, "concealOff" }, - { 29, "crossedOutOff" }, - { 39, "defaultForeground" }, - { 49, "defaultBackground" } - // Add more entries as needed + { 0, "None" }, + { 1, "Bold" }, + { 2, "Dim" }, + { 3, "Italic" }, + { 4, "Underline" }, + { 5, "SlowBlink" }, + { 6, "RapidBlink" }, + { 7, "Invert" }, + { 8, "Conceal" }, + { 9, "Strikethrough" }, + { 21, "BoldOff" }, + { 22, "NormalIntensity" }, + { 23, "ItalicOff" }, + { 24, "UnderlineOff" }, + { 25, "BlinkOff" }, + { 27, "InvertOff" }, + { 28, "ConcealOff" }, + { 29, "StrikethroughOff" } + // Add more entries as needed }; } public class Parser @@ -141,7 +139,7 @@ private static VT.VtCode NewDecoVT(int firstCode, int placement) } private static VT.VtCode NewVT(int firstCode, string[] codeParts, int placement) { - if (firstCode >= 30 && firstCode <= 37 || firstCode >= 40 && firstCode <= 47 || firstCode >= 90 && firstCode <= 97) + if (firstCode >= 30 && firstCode <= 37 || firstCode >= 40 && firstCode <= 47 || firstCode >= 90 && firstCode <= 97 || firstCode >= 100 && firstCode <= 107) { return New4BitVT(firstCode, placement); } diff --git a/PwshSpectreConsole/public/formatting/Format-SpectreTable.ps1 b/PwshSpectreConsole/public/formatting/Format-SpectreTable.ps1 index 763fdda5..b2bbbeda 100644 --- a/PwshSpectreConsole/public/formatting/Format-SpectreTable.ps1 +++ b/PwshSpectreConsole/public/formatting/Format-SpectreTable.ps1 @@ -64,6 +64,10 @@ function Format-SpectreTable { $table = [Table]::new() $table.Border = [TableBorder]::$Border $table.BorderStyle = [Style]::new(($Color | Convert-ToSpectreColor)) + $tableoptions = @{} + $rowoptions = @{} + # maybe we could do this a bit nicer.. it's just to avoid checking for each row. + $script:scalarDetected = $false if ($Width) { $table.Width = $Width } @@ -71,22 +75,23 @@ function Format-SpectreTable { $table.ShowHeaders = $false } if ($Title) { - $table.Title = [TableTitle]::new($Title, [Style]::new(($Color | Convert-ToSpectreColor))) + # used if scalar type as 'Value' + $tableoptions.Title = $Title } $collector = [System.Collections.Generic.List[psobject]]::new() $strip = '\x1B\[[0-?]*[ -/]*[@-~]' - # [Spectre.Console.AnsiConsole]::Profile.Capabilities.Ansi = false + if ($AllowMarkup) { + $rowoptions.AllowMarkup = $true + } } process { - if ($data -is [array]) { - # add array items individually to the collector - foreach ($entry in $data) { + foreach ($entry in $data) { + if ($entry -is [hashtable]) { + $collector.add([pscustomobject]$entry) + } else { $collector.add($entry) } } - else { - $collector.add($data) - } } end { if ($collector.count -eq 0) { @@ -94,70 +99,26 @@ function Format-SpectreTable { } if ($Property) { $collector = $collector | Select-Object -Property $Property - $property | ForEach-Object { - $table.AddColumn($_) | Out-Null - } + $tableoptions.Property = $Property + $rowoptions.PropertiesSelected = $true } - elseif (($collector[0].PSTypeNames[0] -ne 'PSCustomObject') -And ($standardMembers = Get-DefaultDisplayMembers $collector[0])) { - foreach ($key in $standardMembers.Properties.keys) { - $lookup = $standardMembers.Properties[$key] - $table.AddColumn($lookup.Label) | Out-Null - # $table.Columns[-1].Padding = [Spectre.Console.Padding]::new(0, 0, 0, 0) - if ($lookup.width -gt 0) { - # width 0 is autosize, select the last entry in the column list - # Write-Debug "Label: $($lookup.Label) width to $($lookup.Width)" - $table.Columns[-1].Width = $lookup.Width - } - if ($lookup.Alignment -ne 'undefined') { - $table.Columns[-1].Alignment = [Justify]::$lookup.Alignment - } - } - # this formats the values according to the formatdata so we dont have to do it in the foreach loop. + elseif ($standardMembers = Get-DefaultDisplayMembers $collector[0]) { $collector = $collector | Select-Object $standardMembers.Format + $tableoptions.FormatData = $standardMembers.Properties + $rowoptions.FormatFound = $true } - else { - foreach ($prop in $collector[0].psobject.Properties.Name) { - if (-Not [String]::IsNullOrEmpty($prop)) { - $table.AddColumn($prop) | Out-Null - } - } - } + $table = Add-TableColumns -Table $table -Object $collector[0] @tableoptions foreach ($item in $collector) { - $row = foreach ($cell in $item.psobject.Properties) { - if ($standardMembers -And $cell.value -match $strip) { - # we are dealing with an object that has VT codes and a formatdata entry. - # this returns a spectre.console.text/markup object with the VT codes applied. - ConvertTo-SpectreDecoration $cell.value -AllowMarkup:$AllowMarkup - continue - } - if ($null -eq $cell.Value) { - if($AllowMarkup) { - [Markup]::new(" ") - } else { - [Text]::new(" ") - } - } - elseif (-Not [String]::IsNullOrEmpty($cell.Value.ToString())) { - if($AllowMarkup) { - [Markup]::new($cell.Value.ToString()) - } else { - [Text]::new($cell.Value.ToString()) - } - } - else { - if($AllowMarkup) { - [Markup]::new([String]$cell.Value) - } else { - [Text]::new($cell.Value.ToString()) - } - } - } - if($AllowMarkup) { + $row = New-TableRow -Entry $item @rowoptions + if ($AllowMarkup) { $table = [TableExtensions]::AddRow($table, [Markup[]]$row) } else { $table = [TableExtensions]::AddRow($table, [Text[]]$row) } } + if ($Title -And $scalarDetected -eq $false) { + $table.Title = [TableTitle]::new($Title, [Style]::new(($Color | Convert-ToSpectreColor))) + } Write-AnsiConsole $table } } diff --git a/PwshSpectreConsole/public/prompts/Read-SpectreText.ps1 b/PwshSpectreConsole/public/prompts/Read-SpectreText.ps1 index 22409040..af3f8fda 100644 --- a/PwshSpectreConsole/public/prompts/Read-SpectreText.ps1 +++ b/PwshSpectreConsole/public/prompts/Read-SpectreText.ps1 @@ -7,7 +7,7 @@ function Read-SpectreText { ::: .DESCRIPTION - This function uses Spectre Console to prompt the user with a question and returns the user's input. The function takes two parameters: $Question and $DefaultAnswer. $Question is the question to prompt the user with, and $DefaultAnswer is the default answer if the user does not provide any input. + This function uses Spectre Console to prompt the user with a question and returns the user's input. .PARAMETER Question The question to prompt the user with. @@ -21,9 +21,17 @@ function Read-SpectreText { .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 # This will prompt the user with the question "What's your name?" and return the user's input. If the user does not provide any input, the function will return "Prefer not to say". Read-SpectreText -Question "What's your name?" -DefaultAnswer "Prefer not to say" + + .EXAMPLE + # This will prompt the user with the question "What's your favorite color?" and return the user's input. + Read-SpectreText -Question "What's your favorite color?" -AnswerColor "Cyan1" -Choices "Black", "Green","Magenta", "I'll never tell!" #> [Reflection.AssemblyMetadata("title", "Read-SpectreText")] param ( @@ -40,12 +48,11 @@ function Read-SpectreText { if ($DefaultAnswer) { $spectrePrompt = [Spectre.Console.TextPromptExtensions]::DefaultValue($spectrePrompt, $DefaultAnswer) } - if($AnswerColor) { + if ($AnswerColor) { $spectrePrompt.PromptStyle = [Spectre.Console.Style]::new(($AnswerColor | Convert-ToSpectreColor)) } $spectrePrompt.AllowEmpty = $AllowEmpty - if ($null -ne $Choices) - { + if ($null -ne $Choices) { $spectrePrompt = [Spectre.Console.TextPromptExtensions]::AddChoices($spectrePrompt, $Choices) } return Invoke-SpectrePromptAsync -Prompt $spectrePrompt diff --git a/PwshSpectreConsole/public/writing/Write-SpectreCalender.ps1 b/PwshSpectreConsole/public/writing/Write-SpectreCalender.ps1 new file mode 100644 index 00000000..150c8075 --- /dev/null +++ b/PwshSpectreConsole/public/writing/Write-SpectreCalender.ps1 @@ -0,0 +1,79 @@ +using module "..\..\private\completions\Completers.psm1" +using namespace Spectre.Console +function Write-SpectreCalendar { + <# + .SYNOPSIS + Writes a Spectre Console Calendar text to the console. + + .DESCRIPTION + Writes a Spectre Console Calendar text to the console. + + .PARAMETER Date + The date to display the calendar for. + + .PARAMETER Alignment + The alignment of the calendar. + + .PARAMETER Color + The color of the calendar. + + .PARAMETER Border + The border of the calendar. + + .PARAMETER Culture + The culture of the calendar. + + .PARAMETER Events + The events to highlight on the calendar. + Takes a hashtable with the date as the key and the event as the value. + + .PARAMETER HideHeader + Hides the header of the calendar. (Date) + + .EXAMPLE + Write-SpectreCalendar -Date 2024-07-01 -Events @{'2024-07-10' = 'Beach time!'; '2024-07-20' = 'Barbecue' } + + .EXAMPLE + $events = @{ + '2024-01-10' = 'Hello World!' + '2024-01-20' = 'Hello Universe!' + } + Write-SpectreCalendar -Date 2024-01-01 -Events $events + #> + [Reflection.AssemblyMetadata("title", "Write-SpectreCalendar")] + param ( + [datetime] $Date = [datetime]::Now, + [ValidateSet([SpectreConsoleJustify], ErrorMessage = "Value '{0}' is invalid. Try one of: {1}")] + [string] $Alignment = "Left", + [ValidateSpectreColor()] + [ArgumentCompletionsSpectreColors()] + [string] $Color = $script:AccentColor.ToMarkup(), + [ValidateSet([SpectreConsoleTableBorder],ErrorMessage = "Value '{0}' is invalid. Try one of: {1}")] + [string] $Border = "Double", + [cultureinfo] $Culture = [cultureinfo]::CurrentCulture, + [Hashtable]$Events, + [Switch] $HideHeader + ) + $calendar = [Spectre.Console.Calendar]::new($date) + $calendar.Alignment = [Spectre.Console.Justify]::$Alignment + $calendar.Border = [Spectre.Console.TableBorder]::$Border + $calendar.BorderStyle = [Style]::new(($Color | Convert-ToSpectreColor)) + $calendar.Culture = $Culture + $calendar.HeaderStyle = [Style]::new(($Color | Convert-ToSpectreColor)) + $calendar.HighlightStyle = [Style]::new(($Color | Convert-ToSpectreColor)) + if ($HideHeader) { + $calendar.ShowHeader = $false + } + if ($Events) { + foreach ($event in $events.GetEnumerator()) { + # calendar events doesnt appear to support Culture. + $eventDate = $event.Name -as [datetime] + $calendar = [Spectre.Console.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 + } +}