Skip to content

Instantly share code, notes, and snippets.

@jimbrig
Forked from ShaunLawrie/ChatTTY.ps1
Created September 15, 2024 22:57
Show Gist options
  • Save jimbrig/e75fefd75b2db98a9a251f4dca3baac3 to your computer and use it in GitHub Desktop.
Save jimbrig/e75fefd75b2db98a9a251f4dca3baac3 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
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment