Skip to content

Instantly share code, notes, and snippets.

@ShaunLawrie
Last active September 15, 2024 22:57
Show Gist options
  • Save ShaunLawrie/2ed8c580d685598e6faaea60e18d96ea to your computer and use it in GitHub Desktop.
Save ShaunLawrie/2ed8c580d685598e6faaea60e18d96ea to your computer and use it in GitHub Desktop.
ChatTTY - A wireframe of a chat app in PwshSpectreConsole
#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
}
}
}
@ShaunLawrie
Copy link
Author

Caution

This requires the latest prerelease version of the module

chat.mp4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment