Anatomy of a Prompt

First posted: 20 August 2017
Last updated: 30 August 2017

Table of Contents

Introduction

Whenever I post a screen shot of my PowerShell prompt, people ask me how it's put together:

ConEmu ScreenShot

There's a lot going on in this screenshot, so I'm going to put together the pieces. The information here assumes you have general familiarity with customing your PowerShell environment (for example, editing your profile file). It also assumes you have some pre-requisites installed, like Git for Windows and Visual Studio.

You can copy/paste along as we go, but I have also provided downloadable versions at the end of the blog post.

Important Note: When saving the PowerShell files, make sure they are saved as "UTF-8 with Signature". This will ensure that PowerShell treats your file as UTF-8 and does not accidentally treat it as ANSI. Here is an example using my editor of choice, Notepad2:

Encoding in Notepad2

Shell, Font, and Colors

The first thing you'll notice is that I'm using a tabbed command window. The program I'm using is called ConEmu, and I've chosen it because its configuration allows me to get exactly what I want, in a portable executable. (I keep most of my utilities like this in the cloud, so that I can just sync them down to a new machine with no installation.)

The most critical feature that I'm leveraging here is the ability of ConEmu to create a slightly compressed font display without needing an actual condensed font. Speaking of fonts, I use a font from the awesome Nerd Fonts project to provide several of the custom symbols in my prompt. It joins together icons from several projects (including Powerline, Font Awesome, and Octicons), and creates pre-merged versions of their font based on several free, fixed pitch fonts (my prompt is using Ubuntu Mono).

ConEmu Font Settings

I get the slightly condensed look with the "Width" setting (width of 9 against font size of 20).

Lastly, I use a custom color set (based heavily on the Ubuntu default terminal colors). The colors I've chosen for many of the prompt elements are based on the mix of these custom colors; if you choose to stick with the default color values, you may wish to adjust some of my color choices for better legibility.

ConEmu Colors

Important note: Because there are use of Unicode characters in many of the samples, please make sure your text editor is capable of editing files in UTF-8 mode. If you see ? characters where you expect to see custom symbols, make sure that your file is in UTF-8 mode, and that your text editor is using your custom Nerd Font.

posh-git

The blue portion of the prompt is Git project information, powered by posh-git. This is a PowerShell plugin that supports printing information about the Git repository referenced by the current directory. The great thing about posh-git is that it comes with a tremendous amount of flexibility when formatting the prompt output. Here is what it looks like by default:

Default posh-git Prompt

You configure the prompt by overriding the default values in a configuration object that posh-git sets. Here are the overrides which convert that default prompt into the blue prompt you've seen above:

# Background colors
$baseBackgroundColor = "DarkBlue"
$GitPromptSettings.AfterBackgroundColor = $baseBackgroundColor
$GitPromptSettings.AfterStashBackgroundColor = $baseBackgroundColor
$GitPromptSettings.BeforeBackgroundColor = $baseBackgroundColor
$GitPromptSettings.BeforeIndexBackgroundColor = $baseBackgroundColor
$GitPromptSettings.BeforeStashBackgroundColor = $baseBackgroundColor
$GitPromptSettings.BranchAheadStatusBackgroundColor = $baseBackgroundColor
$GitPromptSettings.BranchBackgroundColor = $baseBackgroundColor
$GitPromptSettings.BranchBehindAndAheadStatusBackgroundColor = $baseBackgroundColor
$GitPromptSettings.BranchBehindStatusBackgroundColor = $baseBackgroundColor
$GitPromptSettings.BranchGoneStatusBackgroundColor = $baseBackgroundColor
$GitPromptSettings.BranchIdenticalStatusToBackgroundColor = $baseBackgroundColor
$GitPromptSettings.DelimBackgroundColor = $baseBackgroundColor
$GitPromptSettings.IndexBackgroundColor = $baseBackgroundColor
$GitPromptSettings.ErrorBackgroundColor = $baseBackgroundColor
$GitPromptSettings.LocalDefaultStatusBackgroundColor = $baseBackgroundColor
$GitPromptSettings.LocalStagedStatusBackgroundColor = $baseBackgroundColor
$GitPromptSettings.LocalWorkingStatusBackgroundColor = $baseBackgroundColor
$GitPromptSettings.StashBackgroundColor = $baseBackgroundColor
$GitPromptSettings.WorkingBackgroundColor = $baseBackgroundColor

# Foreground colors
$GitPromptSettings.AfterForegroundColor = "Blue"
$GitPromptSettings.BeforeForegroundColor = "Blue"
$GitPromptSettings.BranchForegroundColor = "Blue"
$GitPromptSettings.BranchGoneStatusForegroundColor = "Blue"
$GitPromptSettings.BranchIdenticalStatusToForegroundColor = "White"
$GitPromptSettings.DefaultForegroundColor = "Gray"
$GitPromptSettings.DelimForegroundColor = "Blue"
$GitPromptSettings.IndexForegroundColor = "Green"
$GitPromptSettings.WorkingForegroundColor = "Yellow"

# Prompt shape
$GitPromptSettings.AfterText = " "
$GitPromptSettings.BeforeText = "  "
$GitPromptSettings.BranchAheadStatusSymbol = ""
$GitPromptSettings.BranchBehindStatusSymbol = ""
$GitPromptSettings.BranchBehindAndAheadStatusSymbol = ""
$GitPromptSettings.BranchGoneStatusSymbol = ""
$GitPromptSettings.BranchIdenticalStatusToSymbol = ""
$GitPromptSettings.DelimText = " ॥"
$GitPromptSettings.LocalStagedStatusSymbol = ""
$GitPromptSettings.LocalWorkingStatusSymbol = ""
$GitPromptSettings.ShowStatusWhenZero = $false

To use these overrides, place these lines after the Import-Module statement that posh-git added to your PowerShell profile. Once you restart your shell, you should see your new customizations in place (don't worry about the path; we'll be removing that later on):

post-git Customized Prompt

If you want to customize the symbols used in the prompt, the easiest way to find them is by using the Character Map application built into Windows. Just select the Nerd Font you're using while browsing characters, and you can copy them directly from this app into your profile:

Windows Character Map application

prompt Function

When PowerShell wants to display your prompt, it executes the command prompt. By default, this command is provided by a "PowerShell function". Adding this to your Profile will override the default prompt with our custom prompt (I'll break down each section afterward):

set-content Function:prompt {
  $title = (get-location).Path.replace($home, "~")
  $idx = $title.IndexOf("::")
  if ($idx -gt -1) { $title = $title.Substring($idx + 2) }

  $windowsIdentity = [System.Security.Principal.WindowsIdentity]::GetCurrent()
  $windowsPrincipal = new-object 'System.Security.Principal.WindowsPrincipal' $windowsIdentity
  if ($windowsPrincipal.IsInRole("Administrators") -eq 1) { $color = "Red"; }
  else { $color = "Green"; }

  $Host.UI.RawUI.ForegroundColor = $GitPromptSettings.DefaultForegroundColor

  if ($LASTEXITCODE -ne 0) {
      write-host " " -NoNewLine
      write-host "  $LASTEXITCODE " -NoNewLine -BackgroundColor DarkRed -ForegroundColor Yellow
  }

  if ($PromptEnvironment -ne $null) {
      write-host " " -NoNewLine
      write-host $PromptEnvironment -NoNewLine -BackgroundColor DarkMagenta -ForegroundColor White
  }

  if (Get-GitStatus -ne $null) {
      write-host " " -NoNewLine
      Write-VcsStatus
  }

  $global:LASTEXITCODE = 0

  if ((get-location -stack).Count -gt 0) {
    write-host " " -NoNewLine
    write-host (("+" * ((get-location -stack).Count))) -NoNewLine -ForegroundColor Cyan
  }

  write-host " " -NoNewLine
  write-host "PS>" -NoNewLine -ForegroundColor $color

  $host.UI.RawUI.WindowTitle = $title
  return " "
}

Let's break this down into the individual parts of the prompt.

Path in the window title

First, you may have noticed in my original screenshot that I don't put the current path on the prompt; instead, I put it into the window title. I find that keeps the prompt compact and consistent. The first section of my prompt function grabs the current path, and replaces your home folder with ~, stashing away the value into $title so it can be used at the end of the prompt:

$title = (get-location).Path.replace($home, "~")
$idx = $title.IndexOf("::")
if ($idx -gt -1) { $title = $title.Substring($idx + 2) }

# ...

$host.UI.RawUI.WindowTitle = $title

Detecting whether you're admin or not

The second thing we do is determine whether you're running as admin or not (meaning, did your prompt request administrator rights via UAC). We convert this into a color, so we print the trailing PS< in either green (safe, non-admin) or red (danger, admin) so you can quickly see whether your command prompt can get you into extra trouble:

$windowsIdentity = [System.Security.Principal.WindowsIdentity]::GetCurrent()
$windowsPrincipal = new-object 'System.Security.Principal.WindowsPrincipal' $windowsIdentity
if ($windowsPrincipal.IsInRole("Administrators") -eq 1) { $color = "Red"; }
else { $color = "Green"; }

# ...

write-host "PS>" -NoNewLine -ForegroundColor $color

Printing last exit code

Now we're going to start printing pieces of the prompt. The first thing we're going to do is print out the last exit code, if there was a failure. We also clear it, so that we only end up printing it once (PowerShell won't clear it unless you run another external command, so this helps us keep the prompt clean):

if ($LASTEXITCODE -ne 0) {
  write-host " " -NoNewLine
  write-host "  $LASTEXITCODE " -NoNewLine -BackgroundColor DarkRed -ForegroundColor Yellow
}

# ...

$global:LASTEXITCODE = 0

Printing prompt environment

We haven't seen the use of the prompt environment in code yet, but this is where we add something to prompt to indicate we're in a special environment. The example shown up top shows a prompt for Visual Studio 2017; I'll show you the full mechanics of that command later, but the key part here is that the script will set a value that we look for:

if ($PromptEnvironment -ne $null) {
  write-host " " -NoNewLine
  write-host $PromptEnvironment -NoNewLine -BackgroundColor DarkMagenta -ForegroundColor White
}

Printing Git status

Now we'll print the status of our source control (from posh-git), if our current directory happens to be a Git folder:

if (Get-GitStatus -ne $null) {
  write-host " " -NoNewLine
  Write-VcsStatus
}

Printing directory stack information

In PowerShell (as with many other shells), you can use the pushd and popd commands to temporarily change your current directory, and then pop back to where you started. This is a stack, which means you can push arbitrary folders to be popped later. Our prompt prints + to represent the current stack depth:

if ((get-location -stack).Count -gt 0) {
  write-host " " -NoNewLine
  write-host (("+" * ((get-location -stack).Count))) -NoNewLine -ForegroundColor Cyan
}

Here is a quick screenshot of how this works:

Command Prompt Stack Depth

Returning a string

There is an oddity with PowerShell's expectations of the prompt command: it expects it to return a string, which it then prints. If you fail to return a string, then it will print PS> for you. Since we already printed that (in our color of choice), we just return a single space:

return " "

Prompt Environment

The final piece of the puzzle is printing the prompt environment. I have several commands which place the shell into a special mode to be used for specific environments. For example, I have a command called vs2017 which adds Visual Studio environment variables just as though you'd run the Visual Studio Command Prompt, and then adds that information to the printed prompt environment.

Here is the contents of vs2017.ps1:

param(
  [string]$edition,
  [switch]$noWeb = $false
)

if ($PromptEnvironment -ne $null) {
  write-host "error: Prompt is already in a custom environment." -ForegroundColor Red
  exit 1
}

# Try and find a version of Visual Studio in the expected location, since the VS150COMNTOOLS environment variable isn't there any more
$basePath = join-path (join-path ${env:ProgramFiles(x86)} "Microsoft Visual Studio") "2017"

if ((test-path $basePath) -eq $false) {
  write-warning "Visual Studio 2017 is not installed."
  exit 1
}

if ($edition -eq "") {
  $editions = (get-childitem $basePath | where-object { $_.PSIsContainer })
  if ($editions.Count -eq 0) {
    write-warning "Visual Studio 2017 is not installed."
    exit 1
  }
  if ($editions.Count -gt 1) {
    write-warning "Multiple editions of Visual Studio 2017 are installed. Please specify one of the editions ($($editions -join ', ')) with the -edition switch."
    exit 1
  }
  $edition = $editions[0]
}

$path = join-path (join-path (join-path $basePath $edition) "Common7") "Tools"

if ((test-path $path) -eq $false) {
  write-warning "Visual Studion 2017 $edition could not be found."
  exit 1
}

$cmdPath = join-path $path "VsDevCmd.bat"

if ((test-path $cmdPath) -eq $false) {
  write-warning "File not found: $cmdPath"
  exit 1
}

$tempFile = [IO.Path]::GetTempFileName()

cmd /c " `"$cmdPath`" && set > `"$tempFile`" "

Get-Content $tempFile | %{
  if ($_ -match "^(.*?)=(.*)$") {
    Set-Content "env:\$($matches[1])" $matches[2]
  }
}

Remove-Item $tempFile

if ($noWeb -eq $false) {
  $path = join-path (join-path (join-path $basePath $edition) "Web") "External"

  if (test-path $path) {
    $env:path = $env:path + ";" + $path
  } else {
    write-warning "Path $path not found; specify -noWeb to skip searching for web tools"
  }
}

$global:PromptEnvironment = "  2017 "

This script is a little complex because Visual Studio 2017 now allows you to install multiple editions side by side. It looks to see which edition you have installed and uses the VsDevCmd.bat to get the updated environment variables. If you have multiple editions installed, you can use the -edition switch to specify which environment you want.

The final line of the script sets the global PromptEnvironment variable, which we use in our custom prompt function.

I have several of these little scripts to set up different environments. You can use this Visual Studio 2017 script as a starting point to developing your own custom environment scripts. In the downloads below, I have provided versions for Visual Studio 2015, 2017, and Preview.

Downloadable Files

Conclusion

I hope this quick tour through my custom prompt inspires you to make customizations of your own, based on your needs. Being able to carefully craft your prompt to be succinct and quickly glanceable can be very handy when spending significant time in your command window of choice.

Happy hacking!