Skip to content

Instantly share code, notes, and snippets.

@loopyd
Last active October 21, 2023 21:06
Show Gist options
  • Save loopyd/5b44af953bea89ce09de621c0cea3056 to your computer and use it in GitHub Desktop.
Save loopyd/5b44af953bea89ce09de621c0cea3056 to your computer and use it in GitHub Desktop.
[R. O. A. R.] Ridiculous Output Augmentation Routine - A pretty multithreaded PowerShell console-based HUD wrapper for shell commands using runspaces. Customize it as you like. Perfect for long-running operations to show a heads-up display and real time status.
<#
.SYNOPSIS
Ridiculous Output Augmentation Routine - R. O. A. R.
.DESCRIPTION
This function invokes a command. It displays a timer with resource usage while the command is running. It also displays the command name, and entertains the user with an animation.
.PARAMETER Command
The command to invoke.
.PARAMETER Arguments
The arguments to pass to the command.
.EXAMPLE
Invoke-Roar -Command "choco" -Arguments @("install", "7zip")
.NOTES
Roar XD
#>
function Roar {
param (
[Parameter(Mandatory=$true)]
[string]$Command,
[Parameter(Mandatory=$false)]
[string[]]$Arguments
)
# Initialize the queues for the stdout, stderr, and timer
$stdoutQueue = [System.Collections.Queue]::Synchronized((New-Object System.Collections.Queue))
$stderrQueue = [System.Collections.Queue]::Synchronized((New-Object System.Collections.Queue))
$timerQueue = [System.Collections.Queue]::Synchronized((New-Object System.Collections.Queue))
# Create an initial session state with the current environment variables
$initialSessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault()
$initialSessionState.Variables.Add(
[System.Management.Automation.Runspaces.SessionStateVariableEntry]::new(
"env", [System.Environment]::GetEnvironmentVariables(), "Environment variables"
)
)
# Command execution runspace
$cmdExecRunspace = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace($initialSessionState)
$cmdExecRunspace.Open()
$cmdExecPowershell = [System.Management.Automation.PowerShell]::Create()
$cmdExecPowershell.Runspace = $cmdExecRunspace
$currentDirectory = Get-Location
$cmdExecPowershell = $cmdExecPowershell.AddScript({
param(
[Parameter(Mandatory=$true)]
[string]$Command,
[Parameter(Mandatory=$false)]
[string]$argumentString,
[Parameter(Mandatory=$false)]
[string]$WorkingDirectory
)
$exitCode = $null
try {
if ($null -ne $WorkingDirectory) {
if (-not (Test-Path -Path $WorkingDirectory)) {
Write-Error "Working directory does not exist: $WorkingDirectory"
throw [System.IO.DirectoryNotFoundException]::new("Working directory does not exist: $WorkingDirectory")
}
Set-Location -Path $WorkingDirectory
}
Invoke-Expression "& `"$Command`" $argumentString 2>&1" | Write-Information
$exitCode = $LASTEXITCODE
} catch {
# Do nothing
} finally {
[PSCustomObject]@{
ExitCode = $exitCode
}
}
}).AddArgument($Command).AddArgument($($Arguments -join ' ')).AddArgument($currentDirectory)
# Start the command execution runspace
$cmdExecAsync = $cmdExecPowershell.BeginInvoke()
# Set up the output processing runspace
$outputProcessingRunspace = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace($initialSessionState)
$outputProcessingRunspace.Open()
$outputProcessingPowershell = [System.Management.Automation.PowerShell]::Create()
$outputProcessingPowershell.Runspace = $outputProcessingRunspace
$outputProcessingPowershell = $outputProcessingPowershell.AddScript({
param(
[System.IAsyncResult]$ResultAsync,
[System.Management.Automation.PowerShell]$powershell,
[System.Collections.Queue]$StdoutQueue,
[System.Collections.Queue]$StderrQueue
)
while (-not $ResultAsync.IsCompleted) {
# The only thing that we would format as an error is actually an error.
if ($powershell.Streams.Error.Count -gt 0) {
$powershell.Streams.Error.ReadAll().ForEach({ $_.Exception.Message }) | ForEach-Object {
$stderrQueue.Enqueue(($_.ToString() -replace '\x1B\[[0-9;]*m' -replace "`r`n", ""))
}
$powershell.Streams.Error.Clear()
}
# The rest of it gets put on the standard pipeline.
if ($powershell.Streams.Information.Count -gt 0) {
$powershell.Streams.Information.ReadAll().ForEach({ $_.MessageData }) | ForEach-Object {
$stdoutQueue.Enqueue(($_.ToString() -replace '\x1B\[[0-9;]*m' -replace "`r`n", ""))
}
$powershell.Streams.Information.Clear()
}
if ($powershell.Streams.Verbose.Count -gt 0) {
$powershell.Streams.Verbose.ReadAll().ForEach({ $_.Message }) | ForEach-Object {
$stdoutQueue.Enqueue(($_.ToString() -replace '\x1B\[[0-9;]*m' -replace "`r`n", ""))
}
$powershell.Streams.Verbose.Clear()
}
if ($powershell.Streams.Warning.Count -gt 0) {
$powershell.Streams.Warning.ReadAll().ForEach({ $_.Message }) | ForEach-Object {
$stdoutQueue.Enqueue(($_.ToString() -replace '\x1B\[[0-9;]*m' -replace "`r`n", ""))
}
$powershell.Streams.Warning.Clear()
}
if ($powershell.Streams.Debug.Count -gt 0) {
$powershell.Streams.Debug.ReadAll().ForEach({ $_.Message }) | ForEach-Object {
$stdoutQueue.Enqueue(($_.ToString() -replace '\x1B\[[0-9;]*m' -replace "`r`n", ""))
}
$powershell.Streams.Debug.Clear()
}
}
}).AddArgument($cmdExecAsync).AddArgument($cmdExecPowershell).AddArgument($stdoutQueue).AddArgument($stderrQueue)
# Start the output processing runspace
$outputProcessingAsync = $outputProcessingPowershell.InvokeAsync()
# Set up the display runspace with the timer queue
$displayRunspace = [runspacefactory]::CreateRunspace()
$displayRunspace.Open()
$displayPowershell = [System.Management.Automation.PowerShell]::Create()
$displayPowershell.Runspace = $displayRunspace
$displayPowershell = $displayPowershell.AddScript({
param($Command, $queue)
$executableName = [System.IO.Path]::GetFileName($Command)
$executableName = $executableName.Substring(0, [Math]::Min(32, $executableName.Length))
$executableName = $executableName.PadRight(32)
$cursorStates = '|/-\'
$cursorIndex = 0
$startTime = Get-Date
$dragonCycle = 4000
$fireCycle = 1000
$boomCycle = 300
$sequenceCycle = $dragonCycle + $fireCycle + $boomCycle
while ($true) {
$elapsed = [datetime]::Now - $startTime
$sequencePosition = $elapsed.Milliseconds % $sequenceCycle
$queue.Enqueue(@{
Cursor = $cursorStates[$cursorIndex % $cursorStates.Length]
ElapsedStr = $("{0:D2}:{1:D2}:{2:D2}.{3:D3}" -f $elapsed.Hours, $elapsed.Minutes, $elapsed.Seconds, $elapsed.Milliseconds)
ExecutableName = $executableName
DragonEmoji = if ($sequencePosition % $dragonCycle -lt $dragonCycle / 4 -and $sequencePosition % 500 -lt 250) { "🐉" } else { " " }
FireEmoji = if ($sequencePosition % $fireCycle -lt $fireCycle / 1.25 -and $sequencePosition % 500 -lt 250 ) { "🔥" } else { " " }
BoomEmoji = if ($sequencePosition % $boomCycle -lt $boomCycle / 4 -and $sequencePosition % 250 -lt 125) { "💥" } else { " " }
Roaring = if ($elapsed.Milliseconds % 1000 -lt 500) { "blep!" } else { " " }
})
$cursorIndex++
Start-Sleep -Milliseconds 1
}
}).AddArgument($Command).AddArgument($timerQueue)
# Start the display runspace
$displayPowershellAsync = $displayPowershell.InvokeAsync()
# Main thread (draws the HUD and waits for the command to complete)
Write-Host "`n`n`r" -NoNewline
$Top = [System.Console]::CursorTop - 2
[System.Console]::CursorVisible = $false
while (-not $cmdExecAsync.IsCompleted -or $stdoutQueue.Count -gt 0 -or $stderrQueue.Count -gt 0) {
while ($stdoutQueue.Count -gt 0) {
$output = $stdoutQueue.Dequeue()
[System.Console]::SetCursorPosition([System.Console]::CursorLeft, $Top)
Write-Host -NoNewline $("`r" + (" " * ([Console]::BufferWidth - 1)) + "`r")
Write-Host -NoNewline -ForegroundColor Magenta $($output.Substring(0, [Math]::Min($output.Length, [Console]::BufferWidth - 2)) + "`r")
}
while ($stderrQueue.Count -gt 0) {
$output = $stderrQueue.Dequeue()
[System.Console]::SetCursorPosition([System.Console]::CursorLeft, $Top)
Write-Host -NoNewline $("`r" + (" " * ([Console]::BufferWidth - 1)) + "`r")
Write-Host -NoNewline -ForegroundColor Red $($output.Substring(0, [Math]::Min($output.Length, [Console]::BufferWidth - 2)) + "`r")
}
while ($timerQueue.Count -gt 0) {
$outputHashTable = $timerQueue.Dequeue()
[System.Console]::SetCursorPosition([System.Console]::CursorLeft, $Top + 1)
Write-Host -NoNewline -ForegroundColor White -BackgroundColor Red " $([string]::Format("{0,1}", $outputHashTable.Cursor)) "
Write-Host -NoNewline -ForegroundColor Yellow -BackgroundColor DarkRed " $([string]::Format('{0,6}', $outputHashTable.Roaring)) "
Write-Host -NoNewline -ForegroundColor Black -BackgroundColor DarkRed " $([string]::Format('{0,20}', $outputHashTable.ExecutableName)) "
Write-Host -NoNewline -ForegroundColor White -BackgroundColor DarkRed " $([string]::Format('{0,12}', $outputHashTable.ElapsedStr)) "
Write-Host -NoNewline -ForegroundColor Yellow -BackgroundColor Red " $([string]::Format('{0,1}', $outputHashTable.DragonEmoji))"
Write-Host -NoNewline -ForegroundColor Yellow -BackgroundColor Red " $([string]::Format('{0,1}', $outputHashTable.FireEmoji))"
Write-Host -NoNewline -ForegroundColor Yellow -BackgroundColor Red " $([string]::Format('{0,1}', $outputHashTable.BoomEmoji)) `r"
}
}
[System.Console]::SetCursorPosition([System.Console]::CursorLeft, $Top)
Write-Host -NoNewline $("`r" + (" " * ([Console]::BufferWidth - 1)) + "`r")
[System.Console]::SetCursorPosition([System.Console]::CursorLeft, $Top + 1)
Write-Host -NoNewline $("`r" + (" " * ([Console]::BufferWidth - 1)) + "`r")
[System.Console]::SetCursorPosition([System.Console]::CursorLeft, $Top)
[System.Console]::CursorVisible = $true
# Close the output processing runspace
$outputProcessingPowershell.Stop()
$outputProcessingPowershell.Dispose()
$outputProcessingRunspace.Close()
$outputProcessingRunspace.Dispose()
# Close the timer runspace
$displayPowershell.Stop()
$displayPowershell.Dispose()
$displayRunspace.Close()
$displayRunspace.Dispose()
# Stop the command execution runspace
$exitCodeResult = $cmdExecPowershell.EndInvoke($cmdExecAsync)
$exitCode = $null -eq $exitCodeResult.ExitCode ? 0 : $exitCodeResult.ExitCode
if ($exitCode -ne 0) {
Write-Host -ForegroundColor Red "❌ Process exited with code $exitCode"
}
# Merge the changed environment variables from the runspace into the current environment
$runspaceEnvironment = $cmdExecPowershell.AddScript('[System.Environment]::GetEnvironmentVariables()').Invoke()
foreach ($key in $runspaceEnvironment[0].Keys) {
[System.Environment]::SetEnvironmentVariable($key, $runspaceEnvironment[0][$key], [System.EnvironmentVariableTarget]::Process)
}
# Close the command execution runspace
$cmdExecPowershell.Dispose()
$cmdExecRunspace.Close()
$cmdExecRunspace.Dispose()
# Destroy the queues
$stdoutQueue.Clear()
$stderrQueue.Clear()
$timerQueue.Clear()
# Send powershell instances to the garbage collector
$cmdExecPowershell = $null
$outputProcessingPowershell = $null
$displayPowershell = $null
[System.GC]::Collect()
# Return the exit code
return $exitCode
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment