PSReadLine icon indicating copy to clipboard operation
PSReadLine copied to clipboard

Make the output of script block handler formatted and rendered to the console?

Open gogsbread opened this issue 5 years ago • 15 comments

When using programs(less, fzf etc.), that emit to alternate buffers, inside ScriptBlock, there is no display of the program buffer. For Eg:

Set-PSReadLineKeyHandler -Chord Ctrl+o -ScriptBlock { less foo.txt }
Set-PSReadLineKeyHandler -Chord Ctrl+o -ScriptBlock { find . | fzf }

Show no output

Couple of questions a) Why is that? b) What should I do if I need to display them? (Start separate process and redirect stdout from that process?)

gogsbread avatar Jan 20 '20 08:01 gogsbread

Found my own answers. Documenting for reference a) PsReadLine seems to handle pipe outputs differently using [Microsoft.PowerShell.PSConsoleReadLine] primitives. So a normal command like Set-PSReadLineKeyHandler -Chord Ctrl+o -ScriptBlock { Write-Output "Foo" } won't work. b) Starting a new process and waiting for that process to finish shows the alternate buffer from that process. From the example above Set-PSReadLineKeyHandler -Chord Ctrl+o -ScriptBlock { Start-Process -FilePath /usr/bin/less -ArgumentList ./foo.txt -Wait } will work

gogsbread avatar Jan 20 '20 19:01 gogsbread

That's a reasonable workaround, but it shouldn't be necessary. Unrelated, but note that less does not use the alternate screen buffer.

The issue is how PowerShell implements ScriptBlock.Invoke which redirects stdout to capture the output. less honors this redirection and does not run interactively.

I think this is fixable in PSReadLine. This line would need to be changed, but after some quick experiments, I'm not sure how exactly. Maybe @daxian-dbw has an idea.

lzybkr avatar Jan 21 '20 15:01 lzybkr

The only way I can think of is changing scriptBlock.Invoke(k, arg); to the following:

PowerShell.Create(RunspaceMode.CurrentRunspace)
    .AddScript(scriptBlock.ToString())
        .AddArgument(k)
        .AddArgument(arg)
    .AddCommand("Out-Default")
    .Invoke();

By explicitly set the downstream cmdlet to be Out-Default, we force the stdout to not be redirected when executing scriptBlock.ToString().

However, it will be a breaking change if we go with this because the scriptBlock may be a closure created by GetNewClosure(), and scriptBlock.ToString() will lose the information about the closure.

daxian-dbw avatar Jan 21 '20 18:01 daxian-dbw

Can you use Invoke-Command instead?

lzybkr avatar Jan 21 '20 18:01 lzybkr

Yup, that would work for the closure:

PowerShell.Create(RunspaceMode.CurrentRunspace)
    .AddCommand("Invoke-Command")
        .AddParameter("ScriptBlock", scriptBlock)
        .AddParameter("ArgumentList", new[] { k, arg })
    .AddCommand("Out-Default")
    .Invoke();

It will still be a behavior change though, as all the output of the script block will be written out to the console. So for any existing script block handlers that accidentally leak to the output pipe, the user will see those output written out. If we change to this, then the guideline should be don't write anything to the output pipe in the handler. Otherwise, it will go through the formatting code and slow down the handler.

Any other side effects you can think of, @lzybkr?

daxian-dbw avatar Jan 21 '20 18:01 daxian-dbw

It will still be a behavior change though, as all the output of the script block will be written out to the console. So for any existing script block handlers that accidentally leak to the output pipe, the user will see those output written out.

Or purposefully. I've been treating them like void methods, so typing anything is probably going to result in a large wall of StringBuilder spam for me. Not sure how typical that is though.

SeeminglyScience avatar Jan 21 '20 18:01 SeeminglyScience

Alright, reopen this issue as it looks like something we are interested in improving.

daxian-dbw avatar Jan 21 '20 18:01 daxian-dbw

The behavior change is probably OK. Arguments in favor:

  • It's a major version change.
  • It only affects the interactive experience.
  • It only affects custom key bindings, and likely few at that.
  • The workaround isn't that obvious.

I was worried it might affect modules like PSFzf but I don't think it will as that module uses the .Net api to launch fzf.

lzybkr avatar Jan 21 '20 18:01 lzybkr

Note that PSReadLine doesn't know there is text rendered on the console, so when typing anything after the handler execution, the new characters will be rendered at the original cursor position and overwrite the output from the handler. This will be annoying. It's already the case today when commands like Write-Host is used in the handler, but overall it's not too bad given that it behaves like a void method today. Making this change will have it look like we encourage rendering the handler output to the console.

image

daxian-dbw avatar Jan 21 '20 19:01 daxian-dbw

Wouldn't the correct behavior for the OP's purpose be to:

  1. Save the current command.
  2. Cancel the command to clear the prompt. (RevertLine())
  3. Enter the desired command on the commandline via SelfInsert() or similar.
  4. Execute the command (AcceptLine()).
  5. Restore the original command.

This way keybinding handlers can remain void. The desired command could even be a [psconsolereadline]::method() if needed, but should, if complexity requires, be implemented as a PowerShell function. This would resolve the issue of output in the middle of editing.

I'll admit, I do not know if all the required services to implement this are available.

msftrncs avatar Jan 24 '20 04:01 msftrncs

Running via AcceptLine might be an option, but there are likely unintended consequences like unwanted commands in your history.

Also - the command you launch would have to be the last (only?) command in your scriptblock - you couldn't do anything afterwards which could be a deal breaker for some useful extensions.

lzybkr avatar Jan 24 '20 16:01 lzybkr

If it's determined to be the responsibility of the handler writer, they could just pipe to Out-Default themselves. The biggest issue with that currently is that PSRL doesn't notice the cursor position change. So the next key press moves the cursor position back to where PSRL thinks it should be.

FWIW, the handlers being invoked with a null pipe is consistent with similar API's like event subscribers and breakpoint actions.

Running via AcceptLine might be an option, but there are likely unintended consequences like unwanted commands in your history.

For AcceptLine to work, wouldn't the key handler have to return before any action would actually take place? Meaning you'd have to discard the current input completely? Or is that what you're referring to with the next sentence?

SeeminglyScience avatar Jan 24 '20 17:01 SeeminglyScience

Yes, I guess I was hinting that you'd have to return, but a function like AcceptAndGetNext could be used to restore the current input.

lzybkr avatar Jan 24 '20 17:01 lzybkr

Hmm, PSFzf doesn't want to play nice for me under linux under PowerShell Core 7 and I think that it would be awesome if interactive scripts could be invoked using hotkeys, so I would appreciate if this issue could be solved in a way that doesn't require external tools.

Edit: Hmm, it seems like I've managed to solve it for now, but this doesn't seem to be the cleanest workaround:

Set-PSReadLineKeyHandler -Chord Ctrl+f -ScriptBlock {
  # Start-Process -FilePath fzf -Wait

  $history = Get-Content -Raw (Get-PSReadLineOption).HistorySavePath

  $p = [System.Diagnostics.Process]@{StartInfo = @{
    FileName = "fzf";
    Arguments = "--tac --no-sort --bind tab:toggle-sort --height `"25%`"";
    RedirectStandardInput = $true;
    RedirectStandardOutput = $true;
  }}

  $p.Start()
  $p.StandardInput.Write($history)
  $stdout = $p.StandardOutput.ReadToEnd()
  $p.WaitForExit()

  [Microsoft.PowerShell.PSConsoleReadLine]::RevertLine()
  [Microsoft.PowerShell.PSConsoleReadLine]::Insert($stdout.Trim())
}


isti115 avatar Dec 09 '20 22:12 isti115

I adapted the @Isti115 workaround, the behavior is still the same here. PSReadLineKeyHandler could have different default values in his context in further versions to prevent user manipulanting processes under the nail like this example.

function MyFzf {
    $p = [System.Diagnostics.Process]@{StartInfo = @{
      FileName = "fzf";
      Arguments = @(
        "--layout=reverse",
        "--height=85%",
        "--border",
        "--no-sort",
        "--prompt=`"Search Directory> `""
        "--bind=`"ctrl-d:preview-page-down`"",
        "--bind=`"ctrl-u:preview-page-up`"",
        "--preview=`"bat --plain --color=always {}`""
        "--preview-window=`"110`""
        "--color=`"bg+:#293739,bg:#1B1D1E,border:#808080,spinner:#E6DB74,hl:#7E8E91,fg:#F8F8F2,header:#7E8E91,info:#A6E22E,pointer:#A6E22E,marker:#F92672,fg+:#F8F8F2,prompt:#F92672,hl+:#F92671`""
      );
      RedirectStandardOutput = $true;
      WorkingDirectory = $PWD;
    }}

    $p.Start()
    $result = $p.StandardOutput.ReadLine()
    $p.WaitForExit()

    [Microsoft.PowerShell.PSConsoleReadLine]::RevertLine()
    [Microsoft.PowerShell.PSConsoleReadLine]::Insert($result)
    [Microsoft.PowerShell.PSConsoleReadLine]::ClearScreen()
 }

rafaeloledo avatar Dec 12 '23 02:12 rafaeloledo