PSReadLine
PSReadLine copied to clipboard
Make the output of script block handler formatted and rendered to the console?
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?)
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
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.
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.
Can you use Invoke-Command instead?
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?
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.
Alright, reopen this issue as it looks like something we are interested in improving.
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.
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.

Wouldn't the correct behavior for the OP's purpose be to:
- Save the current command.
- Cancel the command to clear the prompt. (
RevertLine()) - Enter the desired command on the commandline via
SelfInsert()or similar. - Execute the command (
AcceptLine()). - 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.
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.
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
AcceptLinemight 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?
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.
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())
}
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()
}