Skip to content

Instantly share code, notes, and snippets.

@mklement0
Last active April 26, 2021 13:11
Show Gist options
  • Save mklement0/290ef7cdbdf0db274d6da64fade46929 to your computer and use it in GitHub Desktop.
Save mklement0/290ef7cdbdf0db274d6da64fade46929 to your computer and use it in GitHub Desktop.
PowerShell sample code that demonstrates a PSReadLine key handler that display meta-information about the command being typed in real time.
<#
License: MIT
Author: Michael Klement <mklement0@gmail.com>
See https://stackoverflow.com/a/67266971/45375 for more information.
#>
# The printable characters to respond to.
$printableChars = [char[]] (0x20..0x7e + 0xa0..0xff)
# The control characters to respond to.
$controlChars = 'Enter', 'Escape', 'Backspace', 'Delete'
# Set up the key handler for all specified characters.
$printableChars + $controlChars | ForEach-Object {
Set-PSReadLineKeyHandler $_ {
param($key, $arg)
$line = $cursor = $null
[Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref] $null, [ref] $cursor)
# Handle the key at hand.
switch ($key.Key) {
'Backspace' { [Microsoft.PowerShell.PSConsoleReadLine]::BackwardDeleteChar(); break }
'Delete' { try { [Microsoft.PowerShell.PSConsoleReadLine]::Delete($cursor, 1) } catch { }; break } # ignore error with empty buffer
'Escape' {
[Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref] $line, [ref] $null)
[Microsoft.PowerShell.PSConsoleReadLine]::Delete($0, $line.Length)
break
}
'Enter' {
# Clear any previous meta-information output, so that it doesn't linger and get mixed with command output.
try {
# !! On conhost.exe (regular console) windows on Windows, [Console]::CursorTop and [Console]::WindowTop are *relative to the scrollback buffer*.
# !! In Windows Terminal and on Unix, [Console]::WindowTop is always 0, and [Console]::CursorTop is relative to the screen height - even in the presence of a scrollback buffer.
Write-Host -NoNewLine (, (' ' * [Console]::WindowWidth) * ([Console]::WindowTop + [Console]::WindowHeight - [Console]::CursorTop - 1) -join "`n")
}
catch { Write-Warning "`nClearing the screen below the current line failed: $_" } # This shouldn't happen.
# !! Workaround for a display bug: If the cursor isn't at the very end of the line, everything to the
# !! right is inexplicably *erased* on submission, even though the submission itself still works fine.
# !! We detect that case and simply fill the entire buffer again, which leaves it drawn correctly on submission.
# !! (Note that [Microsoft.PowerShell.PSConsoleReadLine]::SetCursorPosition($line.Length) does *not* work.)
[Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref] $line, [ref] $cursor)
if ($cursor -ne $line.length) {
[Microsoft.PowerShell.PSConsoleReadLine]::Delete(0, $line.Length)
[Microsoft.PowerShell.PSConsoleReadLine]::Insert($line)
}
# Submit the command.
[Microsoft.PowerShell.PSConsoleReadLine]::AcceptLine()
return # We're done.
}
Default { [Microsoft.PowerShell.PSConsoleReadLine]::Insert($key.KeyChar) }
}
# Get the updated buffer content and cursor position.
[Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref] $cursor)
# Note: To get the *AST* (too), use the following:
# $ast = $tokens = $errors = $cursor = $null
# [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref] $ast, [ref] $tokens, [ref] $errors, [ref] $cursor)
# Determine the meta-informaton to print:
$metaInfo = $key | Out-String
# !! There are TWO PROBLEMS with printing meta-information below the line being edited:
# !! * ON *PASTING* TEXT ONTO THE COMMAND LINE:
# !! * With *true pasting*, available on Windows only, with Ctrl-V, this handler isn't invoked at all.
# !! This isn't much of a problem, as just typing a space afterwards, for instance, will resume display of the meta-information.
# !! * !! With *simulated typing* - right-click on Windows, all forms of pasting on Unix - the meta-information is printed
# !! !! *many times, stacked*, as the keypresses are being simulated, and the cursor line *doesn't show the command being entered*.
# !! !! It is unclear why PSReadLine seems to lose the position of its input line and for now WE CANNOT SOLVE THIS PROBLEM.
# !! * IF THERE AREN'T ENOUGH LINES IN THE WINDOW BELOW THE LINE BEING EDITED TO FIT THE META-INFORMATION,
# !! we need to scroll up to make enough room. Otherwise every keystroke causes the cursor to stay on the last line,
# !! and the meta-information to print *above* it, with each keystroke's meta-info pushing the previous output upward.
# !! The only terminal that does *not* require this and *automatically* scrolls up is conhost.exe (regular console windows) on Windows.
# !! ?? Is this connected to all other terminals not being scrollback-buffer-aware (see above)?
# !! WE CANNOT FULLY FIX THIS PROBLEM, AS WE WOULD HAVE TO TELL PSReadLine TO GET ITS INPUT FROM A DIFFERENT LINE,
# !! NAMELY THE SCROLLED-UPWARD ONE; THE BEST WE CAN DO IS TO ANTICIPATE THE PROBLEM AND CLEAR THE SCREEN
# !! using [Microsoft.PowerShell.PSConsoleReadLine]::ClearScreen() - which is NON-DESTRUCTIVE: the scrollback
# !! buffer isn't cleared, and even the current screen content is merely scrolled out of view.
# !! By using a PSReadLine method to clear the screen, PSReadLine implicitly knows what line to prompt for input on afterwards, i.e. the first.
if ($env:OS -ne 'Windows_NT' -or $env:WT_SESSION) {
# Workaround for all terminals except conhost.exe
if ([Console]::CursorTop + $metaInfo.Count -gt [Console]::WindowTop + [Console]::WindowHeight) {
[Microsoft.PowerShell.PSConsoleReadLine]::ClearScreen()
}
}
# Print the desired information below the line being edited.
# Note:
# * The .PadRight() calls ensure that all lines are fully filled (padded with spaces),
# in order to erase potential remnants from previously displayed information.
# * This is NOT sufficient to deal with *varying line counts* being displayed, however.
Write-Host # blank line
Write-Host -NoNewLine -ForegroundColor Yellow ($metaInfo -split '\r?\n' | ForEach-Object PadRight ([Console]::WindowWidth-1), ' ')
# Set the new cursor position.
[Microsoft.PowerShell.PSConsoleReadLine]::SetCursorPosition($cursor)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment