#!/usr/bin/env pwsh # Create a new dynamic module so we don't pollute the global namespace with our functions and # variables $null = New-Module starship { function Get-Cwd { $cwd = Get-Location $provider_prefix = "$($cwd.Provider.ModuleName)\$($cwd.Provider.Name)::" return @{ # Resolve the actual/physical path # NOTE: ProviderPath is only a physical filesystem path for the "FileSystem" provider # E.g. `Dev:\` -> `C:\Users\Joe Bloggs\Dev\` Path = $cwd.ProviderPath; # Resolve the provider-logical path # NOTE: Attempt to trim any "provider prefix" from the path string. # E.g. `Microsoft.PowerShell.Core\FileSystem::Dev:\` -> `Dev:\` LogicalPath = if ($cwd.Path.StartsWith($provider_prefix)) { $cwd.Path.Substring($provider_prefix.Length) } else { $cwd.Path }; } } function Invoke-Native { param($Executable, $Arguments) $startInfo = New-Object System.Diagnostics.ProcessStartInfo -ArgumentList $Executable -Property @{ StandardOutputEncoding = [System.Text.Encoding]::UTF8; RedirectStandardOutput = $true; RedirectStandardError = $true; CreateNoWindow = $true; UseShellExecute = $false; }; if ($startInfo.ArgumentList.Add) { # PowerShell 6+ uses .NET 5+ and supports the ArgumentList property # which bypasses the need for manually escaping the argument list into # a command string. foreach ($arg in $Arguments) { $startInfo.ArgumentList.Add($arg); } } else { # Build an arguments string which follows the C++ command-line argument quoting rules # See: https://docs.microsoft.com/en-us/previous-versions//17w5ykft(v=vs.85)?redirectedfrom=MSDN $escaped = $Arguments | ForEach-Object { $s = $_ -Replace '(\\+)"','$1$1"'; # Escape backslash chains immediately preceding quote marks. $s = $s -Replace '(\\+)$','$1$1'; # Escape backslash chains immediately preceding the end of the string. $s = $s -Replace '"','\"'; # Escape quote marks. "`"$s`"" # Quote the argument. } $startInfo.Arguments = $escaped -Join ' '; } $process = [System.Diagnostics.Process]::Start($startInfo) # Read the output and error streams asynchronously # Avoids potential deadlocks when the child process fills one of the buffers # https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.process.standardoutput?view=net-6.0#remarks $stdout = $process.StandardOutput.ReadToEndAsync() $stderr = $process.StandardError.ReadToEndAsync() [System.Threading.Tasks.Task]::WaitAll(@($stdout, $stderr)) # stderr isn't displayed with this style of invocation # Manually write it to console if ($stderr.Result.Trim() -ne '') { # Write-Error doesn't work here $host.ui.WriteErrorLine($stderr.Result) } $stdout.Result; } function Enable-TransientPrompt { Set-PSReadLineKeyHandler -Key Enter -ScriptBlock { $previousOutputEncoding = [Console]::OutputEncoding try { $parseErrors = $null [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$null, [ref]$null, [ref]$parseErrors, [ref]$null) if ($parseErrors.Count -eq 0) { $script:TransientPrompt = $true [Console]::OutputEncoding = [Text.Encoding]::UTF8 [Microsoft.PowerShell.PSConsoleReadLine]::InvokePrompt() } } finally { if ($script:DoesUseLists) { # If PSReadline is set to display suggestion list, this workaround is needed to clear the buffer below # before accepting the current commandline. The max amount of items in the list is 10, so 12 lines # are cleared (10 + 1 more for the prompt + 1 more for current commandline). [Microsoft.PowerShell.PSConsoleReadLine]::Insert("`n" * [math]::Min($Host.UI.RawUI.WindowSize.Height - $Host.UI.RawUI.CursorPosition.Y - 1, 12)) [Microsoft.PowerShell.PSConsoleReadLine]::Undo() } [Microsoft.PowerShell.PSConsoleReadLine]::AcceptLine() [Console]::OutputEncoding = $previousOutputEncoding } } } function Disable-TransientPrompt { Set-PSReadLineKeyHandler -Key Enter -Function AcceptLine $script:TransientPrompt = $false } function global:prompt { $origDollarQuestion = $global:? $origLastExitCode = $global:LASTEXITCODE # Invoke precmd, if specified try { if (Test-Path function:Invoke-Starship-PreCommand) { Invoke-Starship-PreCommand } } catch {} # @ makes sure the result is an array even if single or no values are returned $jobs = @(Get-Job | Where-Object { $_.State -eq 'Running' }).Count $cwd = Get-Cwd $arguments = @( "prompt" "--path=$($cwd.Path)", "--logical-path=$($cwd.LogicalPath)", "--terminal-width=$($Host.UI.RawUI.WindowSize.Width)", "--jobs=$($jobs)" ) # We start from the premise that the command executed correctly, which covers also the fresh console. $lastExitCodeForPrompt = 0 if ($lastCmd = Get-History -Count 1) { # In case we have a False on the Dollar hook, we know there's an error. if (-not $origDollarQuestion) { # We retrieve the InvocationInfo from the most recent error using $global:error[0] $lastCmdletError = try { $global:error[0] | Where-Object { $_ -ne $null } | Select-Object -ExpandProperty InvocationInfo } catch { $null } # We check if the last command executed matches the line that caused the last error, in which case we know # it was an internal Powershell command, otherwise, there MUST be an error code. $lastExitCodeForPrompt = if ($null -ne $lastCmdletError -and $lastCmd.CommandLine -eq $lastCmdletError.Line) { 1 } else { $origLastExitCode } } $duration = [math]::Round(($lastCmd.EndExecutionTime - $lastCmd.StartExecutionTime).TotalMilliseconds) $arguments += "--cmd-duration=$($duration)" } $arguments += "--status=$($lastExitCodeForPrompt)" if ([Microsoft.PowerShell.PSConsoleReadLine]::InViCommandMode()) { $arguments += "--keymap=vi" } # Invoke Starship $promptText = if ($script:TransientPrompt) { $script:TransientPrompt = $false if (Test-Path function:Invoke-Starship-TransientFunction) { Invoke-Starship-TransientFunction } else { "$([char]0x1B)[1;32m❯$([char]0x1B)[0m " } } else { Invoke-Native -Executable ::STARSHIP:: -Arguments $arguments } # Set the number of extra lines in the prompt for PSReadLine prompt redraw. Set-PSReadLineOption -ExtraPromptLineCount ($promptText.Split("`n").Length - 1) # Return the prompt $promptText # Propagate the original $LASTEXITCODE from before the prompt function was invoked. $global:LASTEXITCODE = $origLastExitCode # Propagate the original $? automatic variable value from before the prompt function was invoked. # # $? is a read-only or constant variable so we can't directly override it. # In order to propagate up its original boolean value we will take an action # which will produce the desired value. # # This has to be the very last thing that happens in the prompt function # since every PowerShell command sets the $? variable. if ($global:? -ne $origDollarQuestion) { if ($origDollarQuestion) { # Simple command which will execute successfully and set $? = True without any other side affects. 1+1 } else { # Write-Error will set $? to False. # ErrorAction Ignore will prevent the error from being added to the $Error collection. Write-Error '' -ErrorAction 'Ignore' } } } # Disable virtualenv prompt, it breaks starship $ENV:VIRTUAL_ENV_DISABLE_PROMPT=1 $script:TransientPrompt = $false $script:DoesUseLists = (Get-PSReadLineOption).PredictionViewStyle -eq 'ListView' if ($PSVersionTable.PSVersion.Major -gt 5) { $ENV:STARSHIP_SHELL = "pwsh" } else { $ENV:STARSHIP_SHELL = "powershell" } # Set up the session key that will be used to store logs $ENV:STARSHIP_SESSION_KEY = -join ((48..57) + (65..90) + (97..122) | Get-Random -Count 16 | ForEach-Object { [char]$_ }) # Invoke Starship and set continuation prompt Set-PSReadLineOption -ContinuationPrompt ( Invoke-Native -Executable ::STARSHIP:: -Arguments @( "prompt", "--continuation" ) ) try { Set-PSReadLineOption -ViModeIndicator script -ViModeChangeHandler { [Microsoft.PowerShell.PSConsoleReadLine]::InvokePrompt() } } catch {} Export-ModuleMember -Function @( "Enable-TransientPrompt" "Disable-TransientPrompt" ) }