Last active
September 15, 2024 22:57
-
-
Save ShaunLawrie/2ed8c580d685598e6faaea60e18d96ea to your computer and use it in GitHub Desktop.
ChatTTY - A wireframe of a chat app in PwshSpectreConsole
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#Requires -Modules @{ ModuleName = 'PwshSpectreConsole'; RequiredVersion = '2.1.1' } | |
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-SpectrePanel -Expand | |
$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 ` | |
| New-SpectreGridRow | |
} 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 ` | |
| New-SpectreGridRow | |
} | |
} | |
# Stack is LIFO, so we need to reverse it to display the messages in the correct order | |
[array]::Reverse($rows) | |
# Just getting the last few for the demo, each single line message takes up about 3 lines of space | |
$rowCount = [int] (($Host.UI.RawUI.WindowSize.Height - 10) / 3) | |
$component = $rows | Select-Object -Last $rowCount | Format-SpectreGrid | 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." } | |
} | |
# 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 = [Console]::ReadKey($true) | |
if ($lastKeyPressed.Key -eq [System.ConsoleKey]::C -and $lastKeyPressed.Modifiers -eq [System.ConsoleModifiers]::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 [System.ConsoleKey]::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 | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Caution
This requires the latest prerelease version of the module
chat.mp4