Published in Articles on by Michiel van Oosterhout ~ 4 min read

In this, the final article of the 3rd series of articles about command-line prompts, we'll finish the configuration of a custom Windows PowerShell prompt. At the end of this article, we will have a consistent, dynamic prompt in Git Bash, Bash in WSL, Windows Command Processor, and Windows PowerShell.

Setting the prompt

After part 1 we now have a profile that automatically sources any installed script that was tagged profile, and we have installed one such script that contains the Get-PromptDir, Get-GitAt, and Set-Color functions. All that is left is a prompt function that will return a custom prompt if $Env:PS_PROMPT is set.

# Get the prompt script
$path = (Get-InstalledScript -Name prompt).InstalledLocation
$path = Join-Path -Path $path -ChildPath "prompt.ps1"

# Declare the script contents
$script = @'
function Prompt
{
    if (-not $Env:PS_PROMPT)
    {
        return "PS $($executionContext.SessionState.Path.CurrentLocation)>"
    }

    # Set environment variables for prompt
    $Env:PROMPT_DIR = Get-PromptDir
    $Env:PROMPT_GIT_AT = Get-GitAt

    $expression = "Get-Prompt $Env:PS_PROMPT"
    $result = Invoke-Expression $expression

    return "`r`n$result "
}
function Get-Prompt
{
    # The prompt string
    $prompt = ""

    # A string holding a segment separator
    $separator = ""

    # Initially the 'previous' background color is the terminal background color
    $prevBg = "-"

    # Initially there was no previous segment and thus no previous sub-segment
    $prevSegment = $false
    $prevSubSegment = $false

    # This acts as if the previous arg was a separator (e.g '/')
    $startSegment = $true

    # Counts sub-segments
    $count = 0

    # Loop over all items
    foreach ($item in $args)
    {
        # Handle separator
        if ($item -eq "/")
        {
            # This signals the next iteration to interpret $item as colors
            $startSegment = $true

            # Continue with next parameter
            continue
        }

        # Handle colors
        if ($startSegment)
        {
            # Interpret $item as colors
            $bg = $item.SubString(0, 1)
            $fg = $item.SubString(1, 1)

            if ($prevSegment)
            {
                # Prepare a 'connecting' separator to be rendered later
                $separator = Set-Color "$bg$prevBg"
                $separator += [char]0xE0B0
                $separator += Set-Color "$bg$fg"
            }

            # Prepare for rendering sub-segments
            $prompt += Set-Color "$bg$fg"

            # Ensure the next iteration does not interpret $item as colors
            $startSegment = $false

            # Ensure the next iteration does not add a sub-separator
            $prevSubSegment = $false

            # Continue with next parameter
            continue
        }

        # Counting sub-segments
        $count = $count + 1

        # Handle sub-segments
        if ("$item")
        {
            if ($separator)
            {
                # Render the separator
                $prompt += $separator

                # Reset the separator until the next segment
                $separator = ""
            }

            # Ensure next separator will 'connect' to this segment
            $prevBg = $bg

            if ($prevSubSegment)
            {
                # Render a sub-separator
                $prompt += Set-Color "$bg-"
                $prompt += [char]0xE0B1
                $prompt += Set-Color "$bg$fg"
            }

            # Ensure the next iteration adds a sub-separator
            $prevSubSegment = $true

            # Append the sub-segment with 1 space of padding
            $prompt += " "
            $prompt += "$item"
            $prompt += " "

            # Ensure a separator is added eventually
            $prevSegment = $true

            # Continue with next parameter
            continue
        }
    }

    if ($prevSegment)
    {
        # Append the final separator
        $prompt += Set-Color "0$prevBg"
        $prompt += [char]0xE0B0
        $prompt += Set-Color "00"
    }

    return $prompt
}
'@

# Append the script contents
Add-Content -Path $path -Value $script

The Prompt function will return the same value as the built-in prompt function, unless Env:PS_PROMPT is set. Then it will first prepare any PROMPT_ environment variables that may be used in $Env:PS_PROMPT, and then it will build the prompt string using the same logic we've seen in the Bash and Windows Command Processor incarnations.

Custom prompts in Git Bash, Windows Command Processor, and Windows PowerShell
Custom prompts in Git Bash, Windows Command Processor, and Windows PowerShell

To set $Env:PS_PROMPT you can run cmd /c setx PS_PROMPT "bW `$Env:PROMPT_DIR / Y- `$Env:PROMPT_GIT_AT". Notice that you need to escape the $ to prevent the environment variables from being expanded once at the time of setting $Env:PS_PROMPT.

Conclusion

Windows PowerShell's use of a prompt function and its profile system makes it relatively easy to configure a custom prompt. Part of the solution presented was built using PowerShellGet's script package (namely the installation of a tagged script). As with the Windows Command Processor solution, this solution adds significant latency to the Windows PowerShell startup, but a lot less latency to the rendering of each prompt.