Files
galaxy/app/assets/bundled/bootstrap/pwsh.ps1
T

1059 lines
46 KiB
PowerShell

[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseApprovedVerbs', '', Scope = 'Function', Target = 'Warp-*', Justification = 'Warp-* functions are ours')]
param()
# Wrap things in a module to avoid cluttering the global scope. We assign it to '$null' to suppress
# the console output from creating the module.
# NOTE: If you do need a function to be global and also have access to variables in this scope, add
# the function name to the 'Export-ModuleMember' call at the end.
$null = New-Module -Name Warp-Module -ScriptBlock {
# Byte sequence used to signal the start of an OSC for Warp JSON messages.
$oscStart = "$([char]0x1b)]9278;"
# Appended to $oscStart to signal that the following message is JSON-encoded.
$oscJsonMarker = 'd'
$oscParamSeparator = ';'
# Byte used to signal the end of an OSC for Warp JSON messages.
$oscEnd = "$([char]0x07)"
# Writes a hex-encoded JSON message to the PTY.
function Warp-Send-JsonMessage([System.Collections.Hashtable]$table) {
$json = ConvertTo-Json -InputObject $table -Compress
# Sends a message to the controlling terminal as an OSC control sequence.
# TODO(CORE-2718): Determine if we need to hex encode the payload.
# Note that because the JSON string may contain characters that we don't control (including
# unicode), we encode it as hexadecimal string to avoid prematurely calling unhook if
# one of the bytes in JSON is 9c (ST) or other (CAN, SUB, ESC).
$encodedMessage = Warp-Encode-HexString $json
Write-Host -NoNewline "$oscStart$oscJsonMarker$oscParamSeparator$encodedMessage$oscEnd"
}
# This script block contains commands and constants that are needed in background threads.
# If you want to be able to use it in a background thread, stick it in this block
$warpCommon = {
# OSC used to mark the start of in-band command output.
#
# Printable characters received this OSC and oscEndGeneratorOutput are parsed and handled as
# output for an in-band command.
$oscStartGeneratorOutput = "$([char]0x1b)]9277;A$oscEnd"
# OSC used to mark the end of in-band command output.
#
# Printable characters received between oscStartGeneratorOutput and this are parsed and
# handled as output for an in-band command.
$oscEndGeneratorOutput = "$([char]0x1b)]9277;B$oscEnd"
$oscResetGrid = "$([char]0x1b)]9279$oscEnd"
function Warp-Send-ResetGridOSC() {
Write-Host -NoNewline $oscResetGrid
}
# Safely attempt to get Node.js version if available. Avoid literal 'node' invocation
# to satisfy PSUseCompatibleCommands across target platforms.
function Warp-TryGet-NodeVersion {
try {
$cmd = Get-Command -CommandType Application node 2>$null
if ($null -eq $cmd) { return '' }
$nv = & $cmd.Source --version 2>$null
if ($null -ne $nv -and "$nv" -ne '') {
return $nv
}
} catch {
# Log at verbose level so normal users are not spammed, but the catch is not empty.
Write-Verbose "node --version failed: $($_.Exception.Message)"
}
return ''
}
# Encode a string as hex-encoded UTF-8.
function Warp-Encode-HexString([string]$str) {
[BitConverter]::ToString([System.Text.Encoding]::UTF8.GetBytes($str)).Replace('-', '')
}
# Hex-encodes the given argument and writes it to the PTY, wrapped in the OSC
# sequences for generator output.
#
# The payload of the OSC is "<content_length>;<hex-encoded content>".
function Warp-Send-GeneratorOutputOsc {
param([string]$message)
$hexEncodedMessage = Warp-Encode-HexString $message
$byteCount = [System.Text.Encoding]::ASCII.GetByteCount($hexEncodedMessage)
Write-Host -NoNewline "$oscStartGeneratorOutput$byteCount;$hexEncodedMessage$oscEndGeneratorOutput"
Warp-Send-ResetGridOSC
}
# Do not run this in the main thread. It mucks around with some env vars
function Warp-Run-InBandGenerator {
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingInvokeExpression', '', Justification = 'We actually need it')]
param([string]$commandId, [string]$command)
try {
# We do not have a good way to simultaneously capture
# the command status $? and the command output of our command.
# this is because Invoke-Expression will always set $? to true.
# To get around this, we append a small bit of code to the original
# command that makes Invoke-Expression throw if the last command
# did not succeed.
$modifiedCommand = "$command" + '; if (-Not $?) { throw }'
# We set this immediately before running Invoke-Expression,
# that way it will default to 0
$LASTEXITCODE = 0
# Note: parens are important here. Without them
# parsing order gets messed up on the 2>&1
$rawOutput = Invoke-Expression -Command "$modifiedCommand" 2>&1
$exitCode = $LASTEXITCODE
# If the generator command returns multi-line output,
# we make sure to join the lines together with a newline, so
# they are properly parsed by warp
$stringifiedOutput = $rawOutput -join "$([char]0x0a)"
# This is a best-effort attempt to get an error code.
# We cannot duplicate our error code logic from Warp-Precmd
# b/c Invoke-Expression will swallow the value of $? and always
# return true. So we do our best to return a legit error code
Write-Output "$commandId;$stringifiedOutput;$exitCode"
} catch {
# This catches a terminating error (ex: entering a command that does not exist)
# In this case, we return an error code of 1
Write-Output "$commandId;1;"
}
}
}
# Load the Warp Common functions in the current session
. $warpCommon
function Get-EpochTime {
[decimal]([DateTime]::UtcNow - [DateTime]::new(1970, 1, 1, 0, 0, 0, 0)).Ticks / 1e7
}
function Warp-Bootstrapped {
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'WARP_BOOTSTRAPPED', Justification = 'False positive as we are assigning to global')]
param([decimal]$rcStartTime, [decimal]$rcEndTime)
$envVarNames = (Get-ChildItem env: | Select-Object -ExpandProperty Name | ForEach-Object { 'env:' + $_ }) + `
(Get-Variable | Select-Object -ExpandProperty Name) -join ' '
$aliasesRaw = Get-Command -CommandType Alias | Select-Object -ExpandProperty DisplayName
$aliases = $aliasesRaw -join [Environment]::NewLine
# Only query modules that ship with PowerShell itself. This keeps bootstrap fast by
# avoiding Windows system modules and third-party modules from the Gallery. The rest are
# loaded asynchronously later.
$corePsModules = @(
'Microsoft.PowerShell.*', # All built-in PS modules (cross-platform)
'Microsoft.WSMan.*', # WS-Management (Windows PS)
'CimCmdlets', # CIM/WMI (Windows)
'PackageManagement', # Package management
'PowerShellGet', # Package get
'PSReadLine', # Line editor bundled with PS
'ThreadJob', # Thread jobs (PS 7)
'PSDiagnostics', # PS diagnostics
'PSDesiredStateConfiguration', # DSC (Windows PS)
'PSWorkflow', # Legacy workflow (Windows PS 5)
'PSWorkflowUtility' # Legacy workflow utility (Windows PS 5)
)
$functionNamesRaw = Get-Command -CommandType Function -Module $corePsModules |
Where-Object { -not $_.Name.StartsWith('Warp') } |
Select-Object -ExpandProperty Name
$functionNames = $functionNamesRaw -join [Environment]::NewLine
$builtinsRaw = Get-Command -CommandType Cmdlet -Module $corePsModules | Select-Object -ExpandProperty Name
$builtins = $builtinsRaw -join [Environment]::NewLine
$shellVersion = $PSVersionTable.PSVersion.ToString()
# PowerShell wasn't cross-platform until version 6. Anything before that is definitely on Windows.
$osCategory = if ($PSVersionTable.PSVersion.Major -le 5) {
'Windows'
} elseif ($IsLinux) {
'Linux'
} elseif ($IsMacOS) {
'MacOS'
} elseif ($IsWindows) {
'Windows'
} else {
''
}
# We do not have an equivalent to 'compgen -k' here, so we are dropping
# in a hardcoded list. List is take from
# https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_reserved_words?view=powershell-7.4
$PSKeywords = @(
'begin', 'break', 'catch', 'class', 'continue', 'data', 'define',
'do', 'dynamicparam', 'else', 'elseif', 'end', 'enum', 'exit',
'filter', 'finally', 'for', 'foreach', 'from', 'function', 'hidden',
'if', 'in', 'param', 'process', 'return', 'static', 'switch', 'throw',
'trap', 'try', 'until', 'using', 'var', 'while', 'inlinescript',
'parallel', 'sequence', 'workflow'
) -join [environment]::NewLine
$linuxDistribution = $null
if ($osCategory -eq 'Linux') {
$osReleaseFile = if (Test-Path -Path '/etc/os-release') {
'/etc/os-release'
} elseif (Test-Path -Path '/usr/lib/os-release') {
'/usr/lib/os-release'
} else {
$null
}
if ($null -ne $osReleaseFile) {
# This is meant to be the equivalent to the bash command
# cat $os_release_file | sed -nE 's/^NAME="(.*)"$/\1/p'. We filter
# specifically for the Name= line of the osRelease file, and then
# pull out the OS name
$linuxDistribution = switch -Regex -File $osReleaseFile {
'^\s*NAME="(.*)"' {
$Matches[1]
break
}
}
}
}
# TODO(PLAT-681) - finish the information here
# for keywords, see 'Get-Help about_Language_Keywords'
$bootstrappedMsg = @{
hook = 'Bootstrapped'
value = @{
session_id = $global:_warpSessionId
histfile = $(Get-PSReadLineOption).HistorySavePath
shell = 'pwsh'
home_dir = "$HOME"
path = $env:PATH
editor = "$env:EDITOR"
env_var_names = $envVarNames
abbreviations = ''
aliases = $aliases
function_names = $functionNames
builtins = $builtins
keywords = "$PSKeywords"
shell_version = $shellVersion
shell_options = ''
rcfiles_start_time = "$rcStartTime"
rcfiles_end_time = "$rcEndTime"
shell_plugins = ''
os_category = $osCategory
linux_distribution = "$linuxDistribution"
shell_path = (Get-Process -Id $PID).Path
}
}
Warp-Send-JsonMessage $bootstrappedMsg
$global:WARP_BOOTSTRAPPED = 1
}
function Warp-Preexec([string]$command) {
$HOST.UI.RawUI.WindowTitle = $command
$preexecMsg = @{
hook = 'Preexec'
value = @{
session_id = $global:_warpSessionId
command = $command
}
}
Warp-Send-JsonMessage $preexecMsg
Warp-Send-ResetGridOSC
# If this preexec is called for user command, kill ongoing generator command jobs and clean
# up the bookkeeping temp files used to bookkeep.
if (-not "$command" -match '^Warp-Run-GeneratorCommand') {
Warp-Stop-ActiveThread
}
# Clean up any completed warp jobs so they do not show up on the user's 'get-job'
# commands
Warp-Clean-CompletedThread
# Remove any instance of the 'Warp-Run-GeneratorCommand' call from the user's history
Clear-History -CommandLine 'Warp-Run-GeneratorCommand*'
}
function Warp-Finish-Update([string]$updateId) {
$updateMsg = @{
hook = 'FinishUpdate'
value = @{
session_id = $global:_warpSessionId
update_id = $updateId
}
}
Warp-Send-JsonMessage $updateMsg
}
function Warp-Handle-DistUpgrade {
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingInvokeExpression', '', Justification = 'We actually need it')]
param([string]$sourceFileName)
$aptConfig = Get-Command -Type Application apt-config | Select-Object -First 1
& $aptConfig shell '$aptSourcesDir' 'Dir::Etc::sourceparts/d' | Invoke-Expression
$sourceFilePath = "${aptSourcesDir}${sourceFileName}"
if (
-not (Test-Path "${sourceFilePath}.list") -and
-not (Test-Path "${sourceFilePath}.sources") -and
(Test-Path "${sourceFilePath}.list.distUpgrade")
) {
# DO NOT DO THIS. We should never run a command for user with 'sudo'. The only reason this
# is safe here is because we insert this function into the input for the user to determine
# if they want to execute (we never run it on their behalf without their permission).
sudo cp "${sourceFilePath}.list.distUpgrade" "${sourceFilePath}.list"
}
}
# We need this for a few reasons
# 1. We need to make sure the environment variable GIT_OPTIONAL_LOCKS=0.
# See https://stackoverflow.com/questions/71836872/git-environment-variables-on-powershell-on-windows
# for why this is complicated
# 2. We need to make sure that we are calling the Application git, and not
# an alias or cmdlet named Git
#
# NOTE: Inlining this call in the function has a weird side effect of outputting
# an escape sequence '^[i'. Since it made it more convenient to have a wrapper
# function anyway, I have not investigated this, but in case someone is working
# on this in the future, beware attempting to inline this function.
function Warp-Git {
$GIT_OPTIONAL_LOCKS = $env:GIT_OPTIONAL_LOCKS
$env:GIT_OPTIONAL_LOCKS = 0
try {
&(Get-Command -CommandType Application git | Select-Object -First 1) $args
} finally {
$env:GIT_OPTIONAL_LOCKS = $GIT_OPTIONAL_LOCKS
}
}
# Helper function that resets the values of '$?' and
# $LASTEXITCODE. Note that it cannot force '$?' to $true
# if it is currently $false
#
# Make sure when you call this you call it with -ErrorAction SilentlyContinue
# or it will print out error information when it is invoked.
function Warp-Restore-ErrorStatus {
[CmdletBinding()]
param([boolean]$status, [int]$code)
$global:LASTEXITCODE = $code
if ($status -eq $false) {
$PSCmdlet.WriteError([System.Management.Automation.ErrorRecord]::new(
[Exception]::new("$([char]0x00)"),
'warp-reset-error',
[System.Management.Automation.ErrorCategory]::NotSpecified,
$null
))
}
}
# Tracks whether or not powershell is unable to find a command.
# See the $ExecutionContext.InvokeCommand.CommandNotFoundAction where it is set to $true,
# and both $ExecutionContext.InvokeCommand.PostCommandLookupAction and Warp-Precmd where
# it is set to $false.
$script:commandNotFound = $false
function Warp-Configure-PSReadLine {
# Set-PSReadLineKeyHandler is the PowerShell equivalent of zsh's bindkey.
Set-PSReadLineKeyHandler -Chord 'Alt+2' -Function BackwardDeleteLine
# Input reporting. Note that ESC-1 is used instead of ESC-i as for all other shells. This
# is because PowerShell on Windows does some virtual key code translation which depends on
# the selected input language. On languages without an "i" on any key, this translation
# fails and the binding gets dropped.
Set-PSReadLineKeyHandler -Chord 'Alt+1' -ScriptBlock {
$inputBuffer = $null
$cursorPosition = $null
[Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$inputBuffer, [ref]$cursorPosition)
$inputBufferMsg = @{
hook = 'InputBuffer'
value = @{
session_id = $global:_warpSessionId
buffer = $inputBuffer
}
}
Warp-Send-JsonMessage $inputBufferMsg
[Microsoft.PowerShell.PSConsoleReadLine]::BackwardDeleteLine()
# This is triggered after precmd, so output here goes to the "early output" handler,
# i.e. the background block. This clears the line the cursor is on. We clear it out b/c
# at this point, the only stuff in the early output handler is typeahead, and that
# shouldn't be displayed in a background block at all. It should be in the input
# editor. Most shells will automatically emit the correct ANSI escape codes to delete
# the contents of the early output handler when we kill the line editor's buffer.
# However, PowerShell doesn't do this correctly due to cursor position mismatch. So,
# we do it manually here instead.
Write-Host -NoNewline "$([char]0x1b)[2K"
}
# Sets the prompt mode to custom prompt (PS1)
# Is the equivalent of warp_change_prompt_modes_to_ps1 in other shells
Set-PSReadLineKeyHandler -Chord 'Alt+p' -ScriptBlock {
$env:WARP_HONOR_PS1 = '1'
Warp-Redraw-Prompt
}
# Sets the prompt mode to warp prompt
# Is the equivalent of warp_change_prompt_modes_to_warp_prompt in other shells
Set-PSReadLineKeyHandler -Chord 'Alt+w' -ScriptBlock {
$env:WARP_HONOR_PS1 = '0'
Warp-Redraw-Prompt
}
Set-PSReadLineOption -AddToHistoryHandler {
param([string]$line)
if ($line -match '^Warp-Run-GeneratorCommand') {
return $false
}
return $true
}
Warp-Disable-PSPrediction
}
# Force use of the Inline PredictionViewStyle. The ListView style can occasionally cause some
# flickering when using Warp and it doesn't matter what the value of this setting is because
# Warp has its own input editor.
function Warp-Disable-PSPrediction {
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseCompatibleCommands', '', Justification = 'Errors are ignored')]
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingEmptyCatchBlock', '', Justification = 'Errors expected')]
param()
try {
Set-PSReadLineOption -PredictionSource None
Set-PSReadLineOption -PredictionViewStyle InlineView
} catch {
}
}
function Warp-Precmd {
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPositionalParameters', '', Justification = 'Warp-Git should use positionals')]
param([bool]$status, [int]$code)
# Our logic here is:
#
# if $status == True, always set $exitCode to 0
# if $status == False and $script:commandNotFound is true
# (meaning we triggered the CommandNotFoundHandler), set $exitCode to 127
# if $status == False and $LASTEXITCODE is zero, set $exitCode to 1
# else set $exitCode to $LASTEXITCODE
#
# Note that this is not going to be 100% accurate, as some cmdlets will fail
# without setting a $LASTEXITCODE, meaning the $LASTEXITCODE will be stale.
$warpCommandNotFound = $script:commandNotFound
$script:commandNotFound = $false
$exitCode = if ($status) {
0
} elseif ($warpCommandNotFound) {
127
} elseif ($code -eq 0) {
1
} else {
$code
}
$newTitle = (Get-Location).Path
# Replace the literal home dir with a tilde.
if ($newTitle.StartsWith($HOME)) {
$newTitle = '~' + $newTitle.Substring($HOME.length)
}
$HOST.UI.RawUI.WindowTitle = $newTitle
$blockId = $script:nextBlockId++
$nextBlockId = "precmd-${global:_warpSessionId}-$blockId"
$commandFinishedMsg = @{
hook = 'CommandFinished'
value = @{
session_id = $global:_warpSessionId
exit_code = $exitCode
next_block_id = $nextBlockId
}
}
Warp-Send-JsonMessage $commandFinishedMsg
Warp-Send-ResetGridOSC
Warp-Configure-PSReadLine
# If this is being called for a generator command, short circuit and send an unpopulated
# precmd payload (except for pwd), since we don't re-render the prompt after generator commands
# are run.
if ($script:generatorCommand -eq $true) {
# TODO(CORE-2639): handle user PreCmds here
$script:generatorCommand = $false
$precmdMsg = @{
hook = 'Precmd'
value = @{
exit_code = $exitCode
next_block_id = $nextBlockId
pwd = ''
ps1 = ''
git_head = ''
git_branch = ''
virtual_env = ''
conda_env = ''
session_id = $global:_warpSessionId
is_after_in_band_command = $true
}
}
Warp-Send-JsonMessage $precmdMsg
} else {
# TODO(CORE-2678): Figure out resetting bindkeys here
$virtualEnv = ''
$condaEnv = ''
$kubeConfig = ''
$gitHead = ''
$gitBranch = ''
$nodeVersion = ''
# Only fill these fields once we've finished bootstrapping, as the
# blocks created during the bootstrap process don't have visible
# prompts, and we don't want to invoke 'git' before we've sourced the
# user's rcfiles and have a fully-populated PATH.
if ($global:WARP_BOOTSTRAPPED -eq 1) {
if (Test-Path env:VIRTUAL_ENV) {
$virtualEnv = $env:VIRTUAL_ENV
}
if (Test-Path env:CONDA_DEFAULT_ENV) {
$condaEnv = $env:CONDA_DEFAULT_ENV
}
if (Test-Path env:KUBECONFIG) {
$kubeConfig = $env:KUBECONFIG
}
# Compute the Node.js version only when the Node.js Version chip is enabled
# (WARP_PROMPT_NODE_VERSION_ENABLED is '0' when the chip is not shown; default
# enabled when unset) and node is available. Cache the result keyed on the
# current location + PATH so we only spawn node when the directory or PATH
# changes (PATH changes on version-manager switches like `nvm use`).
$nodeChipEnabled = "$env:WARP_PROMPT_NODE_VERSION_ENABLED" -ne '0'
$hasNodeCommand = if ($nodeChipEnabled) { Get-Command -CommandType Application node 2>$null } else { $null }
if ($hasNodeCommand) {
try {
$nodeCacheKey = "$((Get-Location).Path)|$env:PATH"
if ($nodeCacheKey -eq $script:warpNodeVersionCacheKey) {
$nodeVersion = $script:warpNodeVersionCacheValue
} else {
# Walk up from the current directory to find a package.json
$dir = Get-Item -LiteralPath (Get-Location).Path
$foundPackageJson = $false
$packageJsonDir = $null
while ($null -ne $dir) {
$candidate = Join-Path $dir.FullName 'package.json'
if (Test-Path -LiteralPath $candidate) {
$foundPackageJson = $true
$packageJsonDir = $dir.FullName
break
}
$dir = $dir.Parent
}
if ($foundPackageJson) {
# Verify package.json resides within a Git repository by walking up to find a .git directory
$probe = Get-Item -LiteralPath $packageJsonDir
$inGitRepo = $false
while ($null -ne $probe) {
if (Test-Path -LiteralPath (Join-Path $probe.FullName '.git')) {
$inGitRepo = $true
break
}
$probe = $probe.Parent
}
if ($inGitRepo) {
$nodeVersion = Warp-TryGet-NodeVersion
}
}
$script:warpNodeVersionCacheKey = $nodeCacheKey
$script:warpNodeVersionCacheValue = $nodeVersion
}
} catch {
# Log at verbose level so the catch block is not empty and diagnostics are available when needed.
Write-Verbose "Failed to compute Node.js context: $($_.Exception.Message)"
}
}
# We do not inline $hasGitCommand b/c the linter does not like seeing '>'
# in an if statement; it thinks we are trying to do -gt incorrectly.
# Since this is a good warning and we do not want to turn off this lint rule,
# we do a little indirection here
$hasGitCommand = Get-Command -CommandType Application git 2>$null
if ($hasGitCommand) {
# This is deliberately not using || b/c || only works in Powershell >=7
$gitBranchTmp = Warp-Git symbolic-ref --short HEAD 2>$null
if ($null -ne $gitBranchTmp) {
$gitBranch = $gitBranchTmp
$gitHead = $gitBranchTmp
} else {
$gitHeadTmp = Warp-Git rev-parse --short HEAD 2>$null
if ($null -ne $gitHeadTmp) {
$gitHead = $gitHeadTmp
}
}
}
}
$honor_ps1 = "$env:WARP_HONOR_PS1" -eq '1'
$precmdMsg = @{
hook = 'Precmd'
value = @{
exit_code = $exitCode
next_block_id = $nextBlockId
pwd = (Get-Location).Path
# TODO(PLAT-687) - honor the PS1
ps1 = ''
honor_ps1 = $honor_ps1
# TODO(PLAT-687) - pwsh does not by default support rprompt, but
# oh-my-posh does. If there is a way to easily extract the oh-my-posh
# rprompt, we might want to use it here
rprompt = ''
git_head = $gitHead
git_branch = $gitBranch
virtual_env = $virtualEnv
conda_env = $condaEnv
node_version = $nodeVersion
session_id = $global:_warpSessionId
kube_config = $kubeConfig
}
}
Warp-Send-JsonMessage $precmdMsg
}
}
$script:inBandCommandCount = 0
$script:threadInner = @{}
$script:threadOuter = @{}
# The inner runspace pool maintains a pool of runspaces that can execute
# arbitrary commands against the user's current environment without
# writing to the screen. Initialize to minimum of 10 runspaces
# to handle double the number of context chips we currently have
# that use in-band commands
$script:innerRunspacePool = [runspacefactory]::CreateRunspacePool(10, 20)
$script:innerRunspacePool.ApartmentState = [System.Threading.ApartmentState]::STA
$script:innerRunspacePool.ThreadOptions = 'ReuseThread'
$script:innerRunspacePool.Open() | Out-Null
# The outer runspace pool maintains a pool of runspaces that
# share the same host as the user's session. This allows them
# to send OSC commands via Write-Host. These outer runspaces
# handle receiving results from the inner runspaces and formatting
# those results into OSCs.
# Initialized to minimum of 5 runspaces since we currently do not
# run more than one outer command at a time.
$script:outerRunspacePool = [runspacefactory]::CreateRunspacePool(5, 10, $Host)
$script:outerRunspacePool.ApartmentState = [System.Threading.ApartmentState]::STA
$script:outerRunspacePool.ThreadOptions = 'ReuseThread'
$script:outerRunspacePool.Open() | Out-Null
class WarpGeneratorCommand {
[string]$CommandId
[string]$Command
}
function Warp-Run-GeneratorCommandImpl {
param(
[WarpGeneratorCommand[]]$commands
)
$jobNumber = $script:inBandCommandCount++
$batchNumber = 0
$jobs = $commands | ForEach-Object {
$commandId = $_.CommandId
$command = $_.Command
# Creates a powershell instance on one of our inner runspaces
# that first loads all the warp common functions, and then
# executes the in-band generator in the current directory
$ps = [powershell]::Create()
$ps.RunspacePool = $script:innerRunspacePool
$ps.AddScript($warpCommon) | Out-Null
$ps.AddScript({
param([string]$loc, [string]$commandId, [string]$command)
Set-Location $loc
Warp-Run-InBandGenerator -commandId $commandId -command "$command"
}).AddParameters(@($PWD.Path, $commandId, "$command")) | Out-Null
$script:threadInner["Warp-Inner-$jobNumber-$batchNumber"] = $psInner
$batchNumber++
@{
commandId = $commandId
ps = $ps
}
}
# Creates the outer job, which waits on all the inner jobs
# and then sends the results back to Warp via OSC
$psOuter = [powershell]::Create()
$psOuter.RunspacePool = $script:outerRunspacePool
$psOuter.AddScript($warpCommon) | Out-Null
$psOuter.AddScript({
param([object[]]$jobs)
$invocations = $jobs | ForEach-Object {
@{
commandId = $_.commandId
ps = $_.ps
async = $_.ps.BeginInvoke()
}
}
$invocations | ForEach-Object {
$commandId = $_.commandId
$ps = $_.ps
$async = $_.async
$output = "$commandId;1;"
try {
$output = $ps.EndInvoke($async)
} catch {
$output = "$commandId;1;"
}
Warp-Send-GeneratorOutputOsc $output
}
}).AddParameters(@($jobs)) | Out-Null
# Note: we are beginning the invocation, but are explicitly
# not stopping it as we do not want to block the main thread.
$async = $psOuter.BeginInvoke()
$script:threadOuter["Warp-Outer-$jobNumber"] = $psOuter
}
function Warp-Stop-ActiveThread {
$script:threadInner.values | ForEach-Object {
$_.Stop()
}
}
function Warp-Clean-CompletedThread {
# Powershell instances states > 2 are terminal.
# See https://learn.microsoft.com/en-us/dotnet/api/system.management.automation.psinvocationstate
if ($script:threadInner.Count -gt 0) {
$script:threadInner.Keys.Clone() | ForEach-Object {
$thread = $script:threadInner[$_]
$state = [int]$thread.InvocationStateInfo.State
if ($state -gt 2) {
$thread.Dispose()
$script:threadInner.Remove($_)
}
}
}
if ($script:threadOuter.Count -gt 0) {
$script:threadOuter.Keys.Clone() | ForEach-Object {
$thread = $script:threadOuter[$_]
$state = [int]$thread.InvocationStateInfo.State
if ($state -gt 2) {
$thread.Dispose()
$script:threadOuter.Remove($_)
}
}
}
}
function Warp-Run-GeneratorCommand {
[CmdletBinding()]
param(
[parameter(ValueFromRemainingArguments = $true)][string[]]$passedArgs
)
$status = $?
$code = $global:LASTEXITCODE
# Setting this environment variable prevents warp_precmd from emitting the
# 'Block started' hook to the Rust app.
$script:generatorCommand = $true
# TODO(CORE-2639) If we ever start supporting user precmd or preexec
# (which doesn't really exist in powershell, but :shrug:), we need
# to properly handle them here like we do in bashzshfish
# Converts the passed in args to WarpGeneratorCommand objects to group them together
# note that if an odd number of arguments is passed in, the last arg will be silently ignored
[WarpGeneratorCommand[]] $jobs = @()
for ($i = 0; $i -lt $passedArgs.Length; $i += 2) {
$commandId = $passedArgs[$i]
$command = $passedArgs[$i + 1]
if ($null -ne $command) {
$jobs += [WarpGeneratorCommand]@{
commandId = $commandId
command = $command
}
}
}
try {
Warp-Run-GeneratorCommandImpl -commands $jobs
} finally {
# NOTE: for some reason the Warp-Restore-ErrorStatus does not work
# for this function, so we are inlining it in here.
$global:LASTEXITCODE = $code
if ($status -eq $false) {
$PSCmdlet.WriteError([System.Management.Automation.ErrorRecord]::new(
[Exception]::new("$([char]0x00)"),
'warp-reset-error',
[System.Management.Automation.ErrorCategory]::NotSpecified,
$null
))
}
}
}
function Warp-Render-Prompt {
param([bool]$status, [int]$code, [bool]$isGeneratorCommand)
# If this is a generator command, we do not want to recompute
# the prompt, and instead want to return the original prompt.
if ($isGeneratorCommand) {
return $script:lastRenderedPrompt
}
# Reset error code for computing prompt
$global:LASTEXITCODE = $code
if (-not $status) {
# Set's $? to false for the next function call,
# so it can be used for computing the prompt
Write-Error '' -ErrorAction Ignore
}
# Compute prompt and cache it as the last rendered prompt
$basePrompt = & $global:_warpOriginalPrompt
$script:lastRenderedPrompt = $basePrompt
return $basePrompt
}
function Warp-Decorate-Prompt {
param([string]$basePrompt)
$e = "$([char]0x1b)"
# Wrap prompt in Prompt Marker OSCs
$startPromptMarker = "$e]133;A$oscEnd"
$startRPromptMarker = "$e]133;P;k=r$oscEnd"
if ("$env:WARP_HONOR_PS1" -eq '0') {
$endPromptMarker = "$e]133;B$oscEnd$oscResetGrid"
} else {
$endPromptMarker = "$e]133;B$oscEnd"
}
$decoratedPrompt = "$basePrompt"
# We only redecorate the prompt if it is not already decorated
if (-not ($basePrompt -match '^\x1b]133;A')) {
$decoratedPrompt = "$startPromptMarker$basePrompt$endPromptMarker"
# Special case for ohmyposh that prints an rprompt. If it matches the format of ohmyposh
# rprompt, we properly parse it into lprompt and rprompt
if ($basePrompt -match '(?<lprompt>.*)[\x1b]7\s*(?<rprompt>\S.*)[\x1b]8') {
$lprompt = $Matches.lprompt
$rprompt = $Matches.rprompt
$decoratedPrompt = "$startPromptMarker$lprompt$endPromptMarker${e}7$startRPromptMarker$rprompt$endPromptMarker${e}8"
}
}
return $decoratedPrompt
}
$script:dontRunPrecmdForPrompt = $false
# Redraws the prompt. Since our prompt also triggers the precmd hook
# we need to signal that we do not want that to happen
function Warp-Redraw-Prompt {
param()
$y = $Host.UI.RawUI.CursorPosition.Y
$script:dontRunPrecmdForPrompt = $true
try {
[Microsoft.PowerShell.PSConsoleReadLine]::InvokePrompt($null, $y)
} finally {
$script:dontRunPrecmdForPrompt = $false
}
}
function Warp-Prompt {
param()
# We need to capture all the data related to exit codes and such
# as soon as possible for a few reasons
# 1. We need to make sure that these values are as fresh as possible
# and are not impacted by our Warp- functions
# 2. After we finish running Warp-Precmd and Warp-Render-Prompt, we want to set these values
# back to what they were originally
$status = $?
$code = $LASTEXITCODE
$isGeneratorCommand = [bool]($script:generatorCommand -eq $true)
if ($script:dontRunPrecmdForPrompt -ne $true) {
Warp-Precmd -status $status -code $code
}
$script:preexecHandled = $false
$renderedPrompt = Warp-Render-Prompt -status $status -code $code -isGeneratorCommand $isGeneratorCommand
$decoratedPrompt = Warp-Decorate-Prompt -basePrompt $renderedPrompt
$extraLines = ($decoratedPrompt -split "$([char]0x0a)").Length - 1
Set-PSReadLineOption -ExtraPromptLineCount $extraLines
# NOTE: Because we are in the prompt, we do not need to reset
# the $? automatic variable (apparently $prompt does not impact it).
# However, we do need to reset the LASTEXITCODE. If we ever refactor
# this to not use the prompt, then watch out for $?
$global:LASTEXITCODE = $code
return $decoratedPrompt
}
if ((Test-Path env:WARP_INITIAL_WORKING_DIR) -and -not [String]::IsNullOrEmpty($env:WARP_INITIAL_WORKING_DIR)) {
Set-Location $env:WARP_INITIAL_WORKING_DIR 2> $null
Remove-Item -Path env:WARP_INITIAL_WORKING_DIR
}
# In some cases, the Clear-Host command will not interface properly with the blocklist.
# Clear-Host defers to whatever the 'clear' command is defined, and if that command
# is not set up to work with Warp (or has funky other behaviors) it can cause problems.
#
# Specific examples:
# - The default /usr/bin/clear on mac creates a giant, empty block to clear content
# off of the screen.
# - if miniconda is installed on an osx system, the miniconda 'clear' command will be
# invoked for 'Clear-Host', which does not play with Warp and winds up doing nothing.
# Because of the above, we explicitly override both 'Clear-Host' and 'clear' to
# instead send a DCS command to Warp instructing it to clear the blocklist.
# We are explicitly NOT calling the underlying clear implementation:
# 1. B/c traditional clear sends an escape sequence that ends up creating an
# empty block that is the full height of the screen.
# 1. B/c our other bootstrap scripts (bash, zsh, fish) do not.
# If we ever want to call the underlying clear command, we could do so by:
# 1. Capturing it with '$_warp_original_clear = (Get-Command Clear-Host).Definition'
# 2. Invoking it with 'Invoke-Expression $_warp_original_clear'
# TODO(PLAT-781): On windows, these two functions should both clear the visible screen
# AND the scrollback
function Clear-Host() {
$inputBufferMsg = @{
hook = 'Clear'
value = @{
session_id = $global:_warpSessionId
}
}
Warp-Send-JsonMessage $inputBufferMsg
}
function clear() {
$inputBufferMsg = @{
hook = 'Clear'
value = @{
session_id = $global:_warpSessionId
}
}
Warp-Send-JsonMessage $inputBufferMsg
}
function Warp-Finish-Bootstrap {
param([decimal]$rcStartTime, [decimal]$rcEndTime)
# This is the closest we can get in PowerShell to a proper preexec hook. We wrap the
# invocation of PSConsoleHostReadline, and call our preexec hook before returning the
# returned value. This allows us to preserve the any custom implementations of
# PSConsoleHostReadLine.
$script:oldPSConsoleHostReadLine = $function:global:PSConsoleHostReadLine
$function:global:PSConsoleHostReadLine = {
$line = & $script:oldPSConsoleHostReadLine
Warp-Preexec "$line"
$line
}
# This handles the case when a command is not found (ex "ehco foo"). As long as it is a
# user-executed command, we set the $script:commandNotFound variable to $true, so we know
# that the command failed b/c of a command lookup failure.
$ExecutionContext.InvokeCommand.CommandNotFoundAction = {
$commandLine = $MyInvocation.Line
# Only trigger the preexec hook for user-submitted commands
# $EventArgs.CommandOrigin is either 'Runspace' or 'Internal'. Internal commands are run
# automatically by PowerShell internals. Runspace is for user-submitted/configured stuff.
# However, Runspace still includes stuff like the prompt function, PostCommandLookupAction,
# and the stuff we set during this bootstrap. So, add a condition to prevent preexec from
# triggering in those cases. Note that we prefix our own functions with the "Warp-" prefix
# so that we can ignore them here.
if ($EventArgs.CommandOrigin -ne 'Runspace' -or ($commandLine -match '^prompt$|^Warp-')) {
return
}
$script:commandNotFound = $true
}
# This sets up our wrapper around $function:prompt, which runs the precmd hook
# and computes the user's custom prompt.
$function:global:prompt = (Get-Command Warp-Prompt).ScriptBlock
Warp-Bootstrapped -rcStartTime $rcStartTime -rcEndTime $rcEndTime
}
###########################################################
# NOTE: NO non-bootstrap / non-user calls below this line #
###########################################################
# Send a precmd message to the terminal to differentiate between the warp
# bootstrap logic pasted into the PTY and the output of shell startup files.
Warp-Precmd -status $global:? -code $global:LASTEXITCODE
Export-ModuleMember -Function clear, Clear-Host, Get-EpochTime, Warp-Finish-Update, Warp-Handle-DistUpgrade, Warp-Run-GeneratorCommand, Warp-Finish-Bootstrap
}
# Finally, get ready to source the user's RC files. This must be done in the global scope (not
# inside Warp-Module) in order to obey the expected scoping in PowerShell's typical startup process.
. {
$rcStartTime = Get-EpochTime
# Source the user's RC files
# https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_profiles?view=powershell-7.4#profile-types-and-locations
# https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_profiles?view=powershell-7.4#the-profile-variable
foreach ($file in @($PROFILE.AllUsersAllHosts, $PROFILE.AllUsersCurrentHost, $PROFILE.CurrentUserAllHosts, $PROFILE.CurrentUserCurrentHost)) {
if ([System.IO.File]::Exists($file)) {
try {
. $file
} catch {
Write-Host -ForegroundColor Red $_.InvocationInfo.PositionMessage
Write-Host -ForegroundColor DarkRed $_.Exception
}
}
}
# Append additional PATH entries if provided via WARP_PATH_APPEND.
# This happens after we source RC files in case they reset PATH.
if (-not [String]::IsNullOrEmpty($env:WARP_PATH_APPEND)) {
$env:PATH = '{0}{1}{2}' -f $env:PATH, [IO.Path]::PathSeparator, $env:WARP_PATH_APPEND
Remove-Item -Path env:WARP_PATH_APPEND
}
# This is a workaround for oh-my-posh's "transient prompt" feature. When enabled, it causes the
# whole screen to clear on every command execution. It is implemented by overwriting the Enter
# and ctrl-c key handlers. Resetting those back to default effectively disables it.
# TODO(CORE-3234) - Find a workaround which allows transient prompt to work.
$enterHandler = Get-PSReadLineKeyHandler | Where-Object -Property Key -EQ -Value 'Enter'
if ($enterHandler -ne $null -and $enterHandler.Function -eq 'OhMyPoshEnterKeyHandler') {
Set-PSReadLineKeyHandler -Chord Enter -Function AcceptLine
}
$ctrlcHandler = Get-PSReadLineKeyHandler | Where-Object -Property Key -EQ -Value 'Control+c'
if ($ctrlcHandler -ne $null -and $ctrlcHandler.Function -eq 'OhMyPoshCtrlCKeyHandler') {
Set-PSReadLineKeyHandler -Chord 'Control+c' -Function CopyOrCancelLine
}
$rcEndTime = Get-EpochTime
# Capture the current prompt (potentially modified by a profile),
# and then reset the prompt to our current noop prompt.
$global:_warpOriginalPrompt = $function:global:prompt
Warp-Finish-Bootstrap -rcStartTime $rcStartTime -rcEndTime $rcEndTime
Remove-Variable -Name enterHandler, ctrlcHandler, rcStartTime, rcEndTime -Scope global -ErrorAction Ignore
# Restore the process's original execution policy now that the user's RC files have been loaded.
if ($global:_warp_PSProcessExecPolicy -ne $null) {
Set-ExecutionPolicy -Scope Process -ExecutionPolicy $global:_warp_PSProcessExecPolicy
}
}