Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature Request - "pre" execution functionality #14484

Closed
kilasuit opened this issue Dec 23, 2020 · 13 comments
Closed

Feature Request - "pre" execution functionality #14484

kilasuit opened this issue Dec 23, 2020 · 13 comments
Assignees
Labels
Issue-Enhancement the issue is more of a feature request than a bug Resolution-No Activity Issue has had no activity for 6 months or more WG-Engine core PowerShell engine, interpreter, and runtime

Comments

@kilasuit
Copy link
Collaborator

kilasuit commented Dec 23, 2020

Summary of the new feature/enhancement

Ability to have a version of the prompt function that can run prior to each command being run in the command line & allow changing of the prompt within it

This would allow for example for a series of actions to set items in the prompt and host prior to execution (like when you kicked off a command, particularly any long running command)

Proposed technical implementation details (optional)

a mirroring of the prompt function but instead runs before the command issued at the terminal is actually run.

function preprompt {
    Write-Host "[" -NoNewline
    Write-Host (Get-Date -Format "HH:mm:ss") -ForegroundColor Gray -NoNewline
    Write-Host "] [" -NoNewline
    }

The above would on each execution update the prompt with the time that the command was executed at
The same code used in the prompt function below update the prompt with the time that the command was completed at

function prompt {
    Write-Host "[" -NoNewline
    Write-Host (Get-Date -Format "HH:mm:ss") -ForegroundColor Gray -NoNewline
    Write-Host "] [" -NoNewline
    }

Having both is useful for transcriptions/interactive glances/ long running processes & would be a useful improvement to the engine

@kilasuit kilasuit added the Issue-Enhancement the issue is more of a feature request than a bug label Dec 23, 2020
@mklement0
Copy link
Contributor

mklement0 commented Dec 23, 2020

If this is primarily about automatic reflecting the execution duration of commands in the prompt string, you can define your prompt function as follows:

function Prompt {
  # Calculate the previous command's execution duration (time span).
  $durationInfo = if ($he = Get-History -Count 1) {
    # Use a '0.00s' format: duration in *seconds*, with two decimal places.
    ' [{0:N2}s]' -f ($he.EndExecutionTime - $he.StartExecutionTime).TotalSeconds
  }
  # Insert the information into the default prompt string; e.g.:
  #   'PS C:\foo> ' becomes 'PS C:\foo [0.23s]> '
  "PS $($executionContext.SessionState.Path.CurrentLocation)${durationInfo}$('>' * ($nestedPromptLevel + 1)) "
}

@kilasuit
Copy link
Collaborator Author

@mklement0 I already have something similar in my profile, like many others do, but your suggestion doesn't address the ask of When I run a command, I want to be able to rebuild the prompt or set window title of the host or any other required information **prior** to the execution of that command, and not need to wait until **after** the command has completed for the prompt/host window/anything else to issue any updates to these components or to other systems which adding a preprompt functionality into the engine would end up addressing the ask.

Whilst I can technically get round this by the below, this really would be better built in to the engine and would mitigate needing to pass to this function a scriptblock to run

function global:preprompt {
    [CmdletBinding()]
    [alias('pp')]
    param (
        [Parameter()]
        [scriptblock]
        $Scriptblock
    )
    $RunningAction = if ($Scriptblock.ToString().Length -gt 25) {$Scriptblock.ToString().Substring(0,25)} else {$Scriptblock.ToString().Substring(0,($Scriptblock.ToString().Length)) }
    if ($PSVersionTable.PSEdition -match 'Desktop' -or $isWindows) {
        $admin = ((New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator))
        if ($admin -eq $true) {
            Write-Host "[" -nonewline -foregroundcolor DarkGray ; Write-Host "Admin" -nonewline -foregroundcolor Red ; Write-Host "] " -nonewline -foregroundcolor DarkGray
            $Host.UI.RawUI.WindowTitle = "[Admin] " + $WindowTitle + ' - ' + (Get-Date -Format HH:mm:ss) + ' - ' + $RunningAction
        }
        else {
            $host.UI.RawUI.WindowTitle = $WindowTitle + ' - ' + (Get-Date -Format HH:mm:ss) + ' - ' + $RunningAction
        }
    }
    Write-Host "[" -NoNewline ; Write-Host (Get-Date -Format "HH:mm:ss") -ForegroundColor Gray -NoNewline ; Write-Host "] [" -NoNewline ; Write-Host $RunningAction -NoNewline ; Write-Host "]" -NoNewline;  Write-Host ''
    $Scriptblock.Invoke()
}

@mklement0

This comment has been minimized.

@mklement0
Copy link
Contributor

mklement0 commented Dec 25, 2020

Thinking about this some more:

Since the prompt by definition has already printed when you submit your command, the desired functionality isn't prompt-related as such.

I wonder if what you're looking for is better implemented as event hooks exposed via $ExecutionContext.InvokeCommand.*Action properties, analogous to $ExecutionContext.InvokeCommand.PreCommandLookupAction / $ExecutionContext.InvokeCommand.PostCommandLookupAction:

For instance, $ExecutionContext.InvokeCommand.PreExecutionAction and $ExecutionContext.InvokeCommand.PostExecutionAction, which could receive the command line being executed as the event argument.

You can - awkwardly - emulate this behavior with $ExecutionContext.InvokeCommand.PostCommandLookupAction and the prompt function; here's an example:

$ExecutionContext.InvokeCommand.PostCommandLookupAction = {
  if ($global:_preExecHandled) { return }
  $cmdLine = $MyInvocation.Line
  if ($args[1].CommandOrigin -ne 'Runspace' -or $cmdLine -match 'PostCommandLookupAction|^prompt$') { return }
  $global:_preExecHandled = $true; $global:_prevTitle = $host.UI.RawUI.WindowTitle
 $info = "Submitting at $(Get-Date): $cmdLine"
 Write-Host -Foreground Yellow $info
 $host.UI.RawUI.WindowTitle = $info
}

$function:prompt = "$function:prompt; `$global:_preExecHandled = `$false; if (`$global:_prevTitle) { `$host.UI.RawUI.WindowTitle = `$global:_prevTitle }"

Note: Restoring the window title doesn't work on Unix-like platforms, because you can only set the title there.

@SeeminglyScience
Copy link
Collaborator

You can get there today by doing this:

function PSConsoleHostReadLine {
    [Microsoft.PowerShell.PSConsoleReadLine]::ReadLine($Host.Runspace, $ExecutionContext)

    preprompt | Out-Null
}

That'll have the same limitations that in-engine support would have. Aside from changing the window title, I'm not sure how this could be used effectively. A real world example using the technique above would go a long way.

@mklement0
Copy link
Contributor

mklement0 commented Dec 28, 2020

That's a much simpler workaround, @SeeminglyScience, thanks. In the absence of a dedicated event such as $ExecutionContext.InvokeCommand.PreExecutionAction that should work fine (assuming PSReadLine is loaded, which is a reasonable assumption).

Adapted to a simplified version of @kilasuit's workaround:

function PSConsoleHostReadLine {
 
  # Prompt the user for a command line to submit, save it in a variable and
  # pass it through, by enclosing it in (...)
  ($line = [Microsoft.PowerShell.PSConsoleReadLine]::ReadLine($Host.Runspace, $ExecutionContext))

  if ($line.Trim()) { # Only react to non-blank lines.

    # Synthesize status info.
    $info = "Launched [$line] at: $(get-date)"
    
    # Set the window title to the status info...
    $host.UI.RawUI.WindowTitle = $info

    # ... and print the same information to the host (only) ("`e[33m" is yellow).
    # Note: We use Out-Host to strictly print to the host.
    #       Using Write-Host would pollute the information stream (#6).
    $host.UI.SupportsVirtualTerminal ? "`e[033m$info`e[m" : $info | Out-Host

  }
  
}

By using Out-Host rather than Write-Host to print the status information, pollution of the output streams is avoided, while still recording it in transcripts.

It still amounts to visual pollution, however - though that may be desired.

Example screenshot (note the window title and the info printed in yellow):

image

A way to lessen this pollution would be to offer a way to redraw the prompt string in place:
Say you submit Get-Date at prompt PS C:\> and just before execution the entire command line is reprinted in place as, e.g.,
PS C:\ [launched at 12/28/2020 11:31:12]> Get-Date

@kilasuit, is that what you had in mind?

If so, the question is: is this technically feasible, in a manner that doesn't disrupt transcripts?

@SeeminglyScience
Copy link
Collaborator

A way to lessen this pollution would be to offer a way to redraw the prompt string in place:
Say you submit Get-Date at prompt PS C:\> and just before execution the entire command line is reprinted in place as, e.g.,
PS C:\ [launched at 12/28/2020 11:31:12]> Get-Date

That's not impossible, but that would be a very challenging work item to make consistent. You'd have to keep track of exactly how much the prompt initially wrote, clear it, re-write it, and then force PSRL to re-render. Also every custom PSHost would have to copy that logic, as prompt processing is currently left to the host. Add in how different terminal emulators process these types of escape sequences a little bit differently and it's hard to imagine this being implemented consistently.

If so, the question is: is this technically feasible, in a manner that doesn't disrupt transcripts?

I think the only feasible option transcript wise would be to just write the re-rendered prompt normally.

@mklement0
Copy link
Contributor

See also: #15271, which also contains a more complete change-the-window-title-while-a-command-is-running workaround.

@iSazonov iSazonov added the WG-Engine core PowerShell engine, interpreter, and runtime label Apr 20, 2021
@daxian-dbw daxian-dbw added the Needs-Triage The issue is new and needs to be triaged by a work group. label Jun 25, 2021
@daxian-dbw daxian-dbw changed the title Feature Request - "pre"prompt functionality Feature Request - "pre" execution functionality Jun 26, 2021
@daxian-dbw
Copy link
Member

The ask is a function to run before an accepted command line gets executed, so change the title to differentiate from #15104, which asks for a way to call custom functions before evaluating the 'Prompt' function.

@daxian-dbw daxian-dbw self-assigned this Feb 17, 2022
@rkeithhill rkeithhill removed the Needs-Triage The issue is new and needs to be triaged by a work group. label Feb 17, 2022
@bamsammich
Copy link

Any updates on this?

As a former Windows user and admin who now works on unix, I'd love to get back to using PowerShell. However, not having the ability to run hooks in a language-supported way prevents me from using tools that the rest of my team uses like direnv and rtx. It would be great to run N preprompt functions to support entire toolchains. See direnvs bash hook as an example.

@SeeminglyScience
Copy link
Collaborator

Any updates on this?

As a former Windows user and admin who now works on unix, I'd love to get back to using PowerShell. However, not having the ability to run hooks in a language-supported way prevents me from using tools that the rest of my team uses like direnv and rtx. It would be great to run N preprompt functions to support entire toolchains. See direnvs bash hook as an example.

FYI there is already an action that is invoked when the location changes, that may be preferable for those tool chains as they could even take effect mid-script

using namespace System
using namespace System.Management.Automation

$handler = [EventHandler[LocationChangedEventArgs]]{
    param([object] $source, [LocationChangedEventArgs] $eventArgs)
    end {
        Write-Host "Old '$($eventArgs.OldPath)' New '$($eventArgs.NewPath)'"
    }
}

$currentAction = $ExecutionContext.SessionState.InvokeCommand.LocationChangedAction
if ($currentAction) {
    $ExecutionContext.SessionState.InvokeCommand.LocationChangedAction = [Delegate]::Combine($currentAction, $handler)
} else {
    $ExecutionContext.SessionState.InvokeCommand.LocationChangedAction = $handler
}

cd \

@bamsammich
Copy link

FYI there is already an action that is invoked when the location changes, that may be preferable for those tool chains as they could even take effect mid-script

This is exactly what I was looking for, much appreciated @SeeminglyScience!

@microsoft-github-policy-service microsoft-github-policy-service bot added the Resolution-No Activity Issue has had no activity for 6 months or more label Mar 21, 2024
Copy link
Contributor

This issue has not had any activity in 6 months, if there is no further activity in 7 days, the issue will be closed automatically.

Activity in this case refers only to comments on the issue. If the issue is closed and you are the author, you can re-open the issue using the button below. Please add more information to be considered during retriage. If you are not the author but the issue is impacting you after it has been closed, please submit a new issue with updated details and a link to this issue and the original.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Issue-Enhancement the issue is more of a feature request than a bug Resolution-No Activity Issue has had no activity for 6 months or more WG-Engine core PowerShell engine, interpreter, and runtime
Projects
None yet
Development

No branches or pull requests

7 participants