Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Getting powershell current line before ENTER is pressed

I had an idea to write a visualizer that displays an AST of your PowerShell line as you type it. But to do this, the first step would be to get the text of the current line, before it is submitted (before ENTER is pressed), but I can't find an API function or hook to do this. Does one exist?

I am using PowerShell Core 7.1.0 on the new Windows Terminal.

PredictiveSource

It seems like PSReadLine's PredictiveSource option might be able to be used for this, provided that it can be invoked on every letter entry and not just on TAB, but I can't find any info on the type contract for 3rd-party plugins after digging through both the docs and the the C# code...

Set-PSReadLineKeyHandler

As the legendary @mklement0 has suggested, perhaps Set-PSReadLineKeyHandler could be used. It appears to be intended for keybindings, but I'm still wrapping my head around how it could be used to this purpose.

like image 524
xdhmoore Avatar asked Apr 17 '21 08:04

xdhmoore


1 Answers

While there is no official mechanism to respond to each keystroke, you can implement your own by setting up a key handler for each printable character and a select few control characters, via the Set-PSReadLineKeyHandler cmdlet. In the key handler, you can display information about the current state of the input buffer below the input line.

tl;dr:

  • Adapt the code below by modifying the $metaInfo = ... line to determine what information to display live about the command-line being edited, below it.

  • Read about limitations in the next section.


Note:

  • It is the printable characters among the first 256 Unicode code points that the key handler is set up for, which is effectively the set of characters that make up the ISO-8859-1 encoding, itself a subset of Windows-1252[1]. Thus, all ASCII-range letters plus a few accented letters are covered, but Cyrillic ones wouldn't be, for instance. However, you could tailor the list to your needs.

  • For illustration purposes, the code below doesn't try to visualize the AST, but prints information about the key that was just pressed, in the form of a formatted representation of a System.ConsoleKeyInfo instance.

    • Find the line that starts with $metaInfo = in the code below to customize what to display.

    • The command to get the AST (abstract syntax tree) representing the buffer content is:

      $ast = $tokens = $errors = $cursor = $null
      [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref] $ast, [ref] $tokens, [ref] $errors, [ref] $cursor)
      
  • The code works best in regular Windows console windows (conhost.exe); limitations:

    • In Windows Terminal and in Unix terminals, a workaround is needed if the current input line is too close to the bottom edge of the window to accommodate the lines needed for display of the custom information: the screen is cleared, and the prompt is presented on the window's first line.

      • However, the contents of the scrollback buffer are fully preserved, so you can just scroll up to see screen contents that was scrolled out of view, if needed.
    • Pasting commands by simulated typing while the key handler is in effect seems to get the PSReadLine module confused about what the current terminal line is, so multiple printing operations of the custom information can end up stacked on top of each other rather than overwriting each other in place.

      • This can only be avoided on Windows - both in regular console windows and in Windows Terminal - with Ctrl-V pasting, in which case the text is truly pasted on the command line, albeit without triggering the key handler (you must then type another (dummy) character to trigger the key handler based on the pasted content).
      • By contrast, simulated typing, which triggers the problem described, is performed:
        • always in Unix terminals
        • on Windows with right-click pasting
# 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

    if ($env:OS -ne 'Windows_NT' -or $env:WT_SESSION) {
      # Workaround for all terminals except conhost.exe
      # See comments at the top of the answer.
      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)
  }
}

A sample screenshot of the key handler in action:

key handler in action

Note how the information below the command line being edited reflects information about the most recently pressed key, a capital D.


[1] ISO-8859-1 is a subset of Windows-1252 with respect to printable characters in that its 0x80-0x9f range is occupied by the so-called C1 control characters, whereas Windows-1252 contains printable characters in this range (with the exception of code points 0x81, 0x8d, 0x8f, 0x90 and 0x9d, which are undefined).

like image 64
mklement0 Avatar answered Nov 13 '22 12:11

mklement0