Skip to content

Commit

Permalink
Add chat example and a way to get renderable object height
Browse files Browse the repository at this point in the history
  • Loading branch information
ShaunLawrie committed Sep 4, 2024
1 parent 5f30092 commit 06ac17a
Show file tree
Hide file tree
Showing 10 changed files with 311 additions and 15 deletions.
28 changes: 28 additions & 0 deletions PwshSpectreConsole.Docs/src/powershell/Mocks.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
$script:mocks = @{
"Read-SpectrePause" = 1
"Get-LastKeyPressed" = 0
"Get-LastChatKeyPressed" = 0
}

function Read-SpectrePauseMock {
Expand All @@ -28,6 +29,33 @@ function Get-LastKeyPressed {
}
}

function Get-LastChatKeyPressedMock {
$keys = @("H", "e", "l", "l", "o", " ", "w", "o", "r", "l", "d!", "Enter",
"T", "h", "a", "n", "k", "s", " ", "f", "o", "r", " ", "t", "h", "e", " ", "d", "e", "m", "o", "Enter", "ctrl-c")

if ($script:mocks["Get-LastChatKeyPressed"] -eq 0) {
Start-Sleep -Seconds 4
} else {
Start-Sleep -Milliseconds 250
}

$selectedKey = $keys[$script:mocks["Get-LastChatKeyPressed"]]
$script:mocks["Get-LastChatKeyPressed"]++

if ($selectedKey -eq "ctrl-c") {
return @{
Key = "C"
KeyChar = "C"
Modifiers = "Control"
}
}

return @{
Key = $selectedKey
KeyChar = $selectedKey
}
}

function Start-SpectreRecordingMock {
param (
[string] $RecordingType,
Expand Down
16 changes: 14 additions & 2 deletions PwshSpectreConsole.Docs/src/powershell/UpdateDocs.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,24 @@ Import-Module "$PSScriptRoot\..\..\..\PwshSpectreConsole\PwshSpectreConsole.psd1
Import-Module "$PSScriptRoot\Helpers.psm1" -Force
Import-Module "$PSScriptRoot\Mocks.psm1" -Force

# Ignore update tags for these
# Ignore update tags for these, remove them from the list if they are updated this just makes it easy to bypass the "updated" tag
$ignoreUpdatesFor = @(
"Format-SpectreBarChart",
"Format-SpectreBreakdownChart",
"Format-SpectrePanel",
"Format-SpectreTable"
"Format-SpectreTable",
"Get-SpectreDemoEmoji",
"Start-SpectreDemo",
"New-SpectreChartItem",
"Get-SpectreImage",
"Get-SpectreImageExperimental",
"Add-SpectreJob",
"Invoke-SpectreCommandWithProgress",
"Invoke-SpectreCommandWithStatus",
"Invoke-SpectreScriptBlockQuietly",
"Wait-SpectreJobs",
"Read-SpectrePause",
"Get-SpectreEscapedText"
)

# Git user details for github action commits
Expand Down
2 changes: 1 addition & 1 deletion PwshSpectreConsole/PwshSpectreConsole.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ FunctionsToExport = 'Add-SpectreJob', 'Format-SpectreBarChart',
'New-SpectreGridRow', 'Format-SpectreTextPath', 'New-SpectreLayout',
'Format-SpectreAligned', 'Out-SpectreHost', 'Add-SpectreTableRow',
'Invoke-SpectreLive', 'Format-SpectreException',
'Get-SpectreDemoFeatures'
'Get-SpectreDemoFeatures', 'Get-SpectreRenderableSize'

# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export.
CmdletsToExport = @()
Expand Down
20 changes: 14 additions & 6 deletions PwshSpectreConsole/private/Write-SpectreHostInternal.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,31 @@ using module ".\completions\Completers.psm1"
# Functions required for unit testing write-spectrehost
function Write-SpectreHostInternalMarkup {
param (
[Parameter(Mandatory)]
[string] $Message,
[ValidateSet([SpectreConsoleJustify], ErrorMessage = "Value '{0}' is invalid. Try one of: {1}")]
[string] $Justify = "Left",
[switch] $NoNewline,
[switch] $PassThru
)

# Add a newline character to the end of the message if NoNewline is not specified
if (-not $NoNewline) {
$Message = $Message.TrimEnd() + "`n"
# If the message is empty, set it to a space. Spectre Console doesn't like empty strings for this.
if ($Message -eq "") {
$Message = " "
}

$output = [Spectre.Console.Markup]::new($Message)
$output.Justification = [Spectre.Console.Justify]::$Justify
if ($PassThru) {
# NewLine isn't required for PassThru
$output = [Spectre.Console.Markup]::new($Message)
$output.Justification = [Spectre.Console.Justify]::$Justify
return $output
}

if (-not $NoNewline) {
$Message += "`n"
}

$output = [Spectre.Console.Markup]::new($Message)
$output.Justification = [Spectre.Console.Justify]::$Justify

[Spectre.Console.AnsiConsole]::Write($output)
}
176 changes: 175 additions & 1 deletion PwshSpectreConsole/public/live/Invoke-SpectreLive.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ function Get-LastKeyPressed {
while ([Console]::KeyAvailable) {
$lastKeyPressed = [Console]::ReadKey($true)
}
#return $lastKeyPressed
return $lastKeyPressed
}
# Start live rendering the layout
Expand Down Expand Up @@ -150,6 +150,180 @@ Invoke-SpectreLive -Data $layout -ScriptBlock {
Start-Sleep -Milliseconds 200
}
}
.EXAMPLE
# **Example 3**
# This is a simple example of creating a chat application. In this example a different approach is used to render the components, each component has been passed a copy of the context and layout object so it can update itself.
Set-SpectreColors -AccentColor DeepPink1
# Build root layout scaffolding for:
# .--------------------------------.
# | Title | <- Update-TitleComponent will render the title
# |--------------------------------|
# | | <- Update-MessageListComponent will display the list of messages here
# | |
# | Messages |
# | |
# | |
# |--------------------------------|
# | CustomTextEntry | <- Update-CustomTextEntryComponent will create a text entry prompt here that is manually managed by pushing keys into a string
# |________________________________|
$layout = New-SpectreLayout -Name "root" -Rows @(
# Row 1
(New-SpectreLayout -Name "title" -MinimumSize 5 -Ratio 1 -Data ("empty")),
# Row 2
(New-SpectreLayout -Name "messages" -Ratio 10 -Data ("empty")),
# Row 3
(New-SpectreLayout -Name "customTextEntry" -MinimumSize 5 -Ratio 1 -Data ("empty"))
)
# Component functions for rendering the content of each panel
function Update-TitleComponent {
param (
[Spectre.Console.LiveDisplayContext] $Context,
[Spectre.Console.Layout] $LayoutComponent
)
$component = @(
("🧠 ChaTTY" | Format-SpectreAligned -HorizontalAlignment Center -VerticalAlignment Middle | Format-SpectrePadded -Padding 1),
(Write-SpectreRule -LineColor DeepPink1 -PassThru)
) | Format-SpectreRows | Format-SpectrePanel -Border None
$LayoutComponent.Update($component) | Out-Null
$Context.Refresh()
}
function Update-MessageListComponent {
param (
[Spectre.Console.LiveDisplayContext] $Context,
[Spectre.Console.Layout] $LayoutComponent,
[System.Collections.Stack] $Messages
)
$rows = @()
foreach ($message in $Messages) {
if ($message.Actor -eq "System") {
$rows += $message.Message.PadRight(6) `
| Get-SpectreEscapedText `
| Write-SpectreHost -Justify Left -PassThru `
| Format-SpectrePanel -Color Grey -Header "System" `
| Format-SpectreAligned -HorizontalAlignment Left `
| Format-SpectrePadded -Top 0 -Left 10 -Bottom 0 -Right 0
} else {
$rows += $message.Message.PadRight($message.Actor.Length) `
| Get-SpectreEscapedText `
| Write-SpectreHost -Justify Right -PassThru `
| Format-SpectrePanel -Color Pink1 -Header $message.Actor `
| Format-SpectreAligned -HorizontalAlignment Right `
| Format-SpectrePadded -Top 0 -Left 0 -Bottom 0 -Right 10
}
}
# Add the heights of each message until reaching the max size, subtract the height of the title and text entry components (10)
$availableHeight = $Host.UI.RawUI.WindowSize.Height - 10
$totalHeight = 0
$rowsToRender = @()
foreach ($row in $rows) {
$totalHeight += ($row | Get-SpectreRenderableSize).Height
if ($totalHeight -gt $availableHeight) {
break
}
$rowsToRender += $row
}
# Stack is LIFO, so we need to reverse it to display the messages in the correct order
[array]::Reverse($rowsToRender)
$component = $rowsToRender | Format-SpectreRows | Format-SpectreAligned -VerticalAlignment Top | Format-SpectrePanel -Border None
$LayoutComponent.Update($component) | Out-Null
$Context.Refresh()
}
function Update-CustomTextEntryComponent {
param (
[Spectre.Console.LiveDisplayContext] $Context,
[Spectre.Console.Layout] $LayoutComponent,
[string] $CurrentInput
)
$safeInput = [string]::IsNullOrEmpty($CurrentInput) ? "" : ($CurrentInput | Get-SpectreEscapedText)
$component = "[gray]Prompt:[/] $safeInput" | Format-SpectrePanel -Expand | Format-SpectrePadded -Top 0 -Left 20 -Bottom 0 -Right 20 | Format-SpectreAligned -HorizontalAlignment Center
$LayoutComponent.Update($component) | Out-Null
$Context.Refresh()
}
# App logic functions
function Get-SomeChatResponse {
param (
[System.Collections.Stack] $Messages,
[Spectre.Console.LiveDisplayContext] $Context,
[Spectre.Console.Layout] $LayoutComponent
)
# Pretend to be thinking
$ellipsisCount = 1
for ($i = 0; $i -lt 3; $i++) {
$Messages.Push(@{ Actor = "System"; Message = ("." * $ellipsisCount) })
$ellipsisCount++
Update-MessageListComponent -Context $Context -LayoutComponent $LayoutComponent -Messages $Messages
Start-Sleep -Milliseconds 500
# Remove the last thinking message
$null = $Messages.Pop()
}
# Return the response
return @{ Actor = "System"; Message = "I don't understand what you're saying." }
}
function Get-LastChatKeyPressed {
return [Console]::ReadKey($true)
}
# Start live rendering the layout
Invoke-SpectreLive -Data $layout -ScriptBlock {
param (
[Spectre.Console.LiveDisplayContext] $Context
)
# State
$messages = [System.Collections.Stack]::new(@(
@{ Actor = "System"; Message = "👋 Hello, welcome to ChaTTY!" },
@{ Actor = "System"; Message = "Type your message and press Enter to send it." },
@{ Actor = "System"; Message = "Use the Up and Down arrow keys to scroll through previous messages." },
@{ Actor = "System"; Message = "Press 'ctrl-c' to close the chat." }
))
$currentInput = ""
while ($true) {
# Update components
Update-TitleComponent -Context $Context -LayoutComponent $layout["title"]
Update-MessageListComponent -Context $Context -LayoutComponent $layout["messages"] -Messages $messages
Update-CustomTextEntryComponent -Context $Context -LayoutComponent $layout["customTextEntry"] -CurrentInput $currentInput
# Real basic input handling, just add characters and remove if backspace is pressed, submit message if Enter is pressed
[Console]::TreatControlCAsInput = $true
$lastKeyPressed = Get-LastChatKeyPressed
if ($lastKeyPressed.Key -eq "C" -and $lastKeyPressed.Modifiers -eq "Control") {
# Exit the loop. You have to treat ctrl-c as input to avoid the console readkey blocking the sigint
return
} elseif ($lastKeyPressed.Key -eq "Enter") {
# Add the latest user message to the message stack
$messages.Push(@{ Actor = ($env:USERNAME + $env:USER); Message = $currentInput })
$currentInput = ""
Update-CustomTextEntryComponent -Context $Context -LayoutComponent $layout["customTextEntry"] -CurrentInput $currentInput
Update-MessageListComponent -Context $Context -LayoutComponent $layout["messages"] -Messages $messages
$messages.Push((Get-SomeChatResponse -Messages $messages -Context $Context -LayoutComponent $layout["messages"]))
} elseif($lastKeyPressed.Key -eq "Backspace") {
# Remove the last character from the current input string
$currentInput = $currentInput.Substring(0, [Math]::Max(0, $currentInput.Length - 1))
} elseif ($lastKeyPressed.KeyChar) {
# Add the character to the current input string
$currentInput += $lastKeyPressed.KeyChar
}
}
}
#>
function Invoke-SpectreLive {
[Reflection.AssemblyMetadata("title", "Invoke-SpectreLive")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ function Invoke-SpectreCommandWithProgress {
Invokes a Spectre command with a progress bar.
.DESCRIPTION
This function takes a script block as a parameter and executes it while displaying a progress bar. The context and task objects are defined at [https://spectreconsole.net/api/spectre.console/progresscontext/](https://spectreconsole.net/api/spectre.console/progresscontext/).
The context requires at least one task to be added for progress to be displayed. The task object is used to update the progress bar by calling the Increment() method or other methods defined in Spectre console [https://spectreconsole.net/api/spectre.console/progresstask/](https://spectreconsole.net/api/spectre.console/progresstask/).
This function takes a script block as a parameter and executes it while displaying a progress bar. The context and task objects are defined at [https://spectreconsole.net/api/spectre.console/progresscontext/](https://spectreconsole.net/api/spectre.console/progresscontext/).
The context requires at least one task to be added for progress to be displayed. The task object is used to update the progress bar by calling the `Increment()` method or other methods defined in Spectre console [https://spectreconsole.net/api/spectre.console/progresstask/](https://spectreconsole.net/api/spectre.console/progresstask/).
See https://spectreconsole.net/live/progress for more information.
.PARAMETER ScriptBlock
The script block to execute.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ function Invoke-SpectreCommandWithStatus {
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.
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.
See https://spectreconsole.net/live/status for more information.
.PARAMETER ScriptBlock
The script block to invoke.
Expand Down
6 changes: 4 additions & 2 deletions PwshSpectreConsole/public/prompts/Read-SpectrePause.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ function Read-SpectrePause {
Indicates whether to write a newline character before displaying the message. By default, a newline character is written.
.EXAMPLE
# **Example 1**
# This example demonstrates how to use the Read-SpectrePause function.
Read-SpectrePause -Message "Press the [red]enter[/] key to continue, when you press it this message will disappear..."
# Type "↲" to dismiss the message
.EXAMPLE
# **Example 1**
# **Example 2**
# This example demonstrates how to use the Read-SpectrePause function with the AnyKey parameter.
Read-SpectrePause -Message "Press the [red]ANY[/] key to continue, when you press it this message will disappear..."
Read-SpectrePause -Message "Press the [red]ANY[/] key to continue, when you press it this message will disappear..." -AnyKey
# Type "x" to dismiss the message
#>
[Reflection.AssemblyMetadata("title", "Read-SpectrePause")]
Expand Down
Loading

0 comments on commit 06ac17a

Please sign in to comment.