Skip to main content
ansicode
PowerShell

ANSI escape codes in PowerShell — $PSStyle, Write-Host, raw VT

PowerShell 7.2+ ships $PSStyle, a typed automatic variable that is arguably the most modern stdlib colour API across all languages — capability-gated via $PSStyle.OutputRendering (Ansi/PlainText/Host), enumerates every SGR code as named properties ($PSStyle.Foreground.Red, $PSStyle.Bold, $PSStyle.Reset), and integrates with the host so a piped Get-ChildItem strips colour automatically. For PS 5.1 (Windows-default) you fall back to Write-Host -ForegroundColor or raw VT via the ` e ` escape token (PS 7+) / [char]27 (PS 5.1). Windows 10+ Conhost / Windows Terminal both parse ANSI natively — no colorama`-style shim needed.

Recommended libraries

  • $PSStyle (built-in)

    Automatic variable shipped in PS 7.2+. Named SGR properties ($PSStyle.Foreground.Red, $PSStyle.Bold, $PSStyle.Underline) emit the canonical escape sequences without you handcrafting ` e[31m `. Capability gate via $PSStyle.OutputRendering = 'PlainText' | 'Ansi' | 'Host''Host'` (default) auto-strips when the stream goes to a non-TTY sink. No third-party library needed for the SGR-heavy case.

  • Write-Host -ForegroundColor

    PS 5.1 baseline — works on every Windows since 2016 without enabling VT. Takes named ConsoleColor values (Red, Yellow, DarkGray, …) and the host translates them into the appropriate output for the current sink. Trade-off: Write-Host bypasses the output stream, so the coloured text is invisible to |, >, transcript logs, and 4>&1 redirects.

  • PSAnsi

    Community module that backports a $PSStyle-ish ergonomic to PS 5.1 plus adds 256-colour and truecolor helpers (Get-Ansi -ForegroundRgb 255,128,0) that the built-in $PSStyle only covers via the *Rgb style methods on 7.4+. Useful when you target both PS 5.1 and modern PS 7 from one script.

  • oh-my-posh

    Cross-shell prompt engine (PS / Bash / Zsh / Fish / Nu) — emits ANSI-rich themed prompts with git status, AWS context, kubectl context, etc. Pure ANSI / OSC under the hood; theming is JSON-driven, so it's a great reference for what a polished real-world ANSI prompt looks like across the SGR + OSC 8 hyperlink + OSC 133 prompt-mark surface.

Idiomatic patterns

$PSStyle (PS 7.2+) — named SGR properties + Reset
# $PSStyle is an automatic variable — no import, no module install.
# Every named property is a string holding the SGR escape sequence;
# concatenate with text and rely on $PSStyle.Reset to close the run.
# OutputRendering = 'Host' (default) auto-strips when stdout is piped.

if ($PSStyle.OutputRendering -ne 'PlainText') {
  Write-Output "$($PSStyle.Foreground.Red)$($PSStyle.Bold)error:$($PSStyle.Reset) permission denied"
  Write-Output "$($PSStyle.Foreground.Yellow)warn:$($PSStyle.Reset) deprecated flag"
  Write-Output "$($PSStyle.Foreground.BrightGreen)ok:$($PSStyle.Reset) 142 tests passed"
}

# Truecolor (PS 7.4+):
Write-Output "$($PSStyle.Foreground.FromRgb(255,128,0))orange truecolor$($PSStyle.Reset)"

# Capability gate — force-disable in CI / piped contexts you don't trust:
if ($env:NO_COLOR -or -not [Console]::IsOutputRedirected) {
  $PSStyle.OutputRendering = 'Ansi'
} else {
  $PSStyle.OutputRendering = 'PlainText'
}
Raw VT escape — `e[ token (PS 7+) and [char]27 (PS 5.1)
# PS 7+ added the backtick-e escape token, mirroring \e in C strings.
# PS 5.1 (Windows-default) requires the [char]27 numeric-cast form.

# PS 7+ — concise, readable:
Write-Output "`e[1;31mError:`e[0m permission denied"

# PS 5.1 — define once, reuse:
$esc = [char]27
Write-Output "$esc[1;31mError:$esc[0m permission denied"

# Cross-version helper — works on both:
function Esc { param([string]$Body) "$([char]27)[$Body" }
Write-Output "$(Esc '38;2;255;128;0m')orange truecolor$(Esc '0m')"

# Windows note: Conhost on Windows 10 1709+ and Windows Terminal both
# parse VT by default. No SetConsoleMode flip needed unless you target
# Windows 7/8 (and PowerShell 7+ already dropped those).
Capability gate — IsOutputRedirected + NO_COLOR + WT_SESSION
# The canonical PS colour decision. Stack the same signals as the
# Unix world plus the Windows-specific WT_SESSION hint (Windows Terminal
# sets it; legacy Conhost does not — useful for opting into truecolor).

function Test-AnsiCapable {
  if ($env:NO_COLOR) { return $false }
  if ($env:FORCE_COLOR -and $env:FORCE_COLOR -ne '0') { return $true }
  if ([Console]::IsOutputRedirected) { return $false }
  return $true
}

function Test-TruecolorCapable {
  if (-not (Test-AnsiCapable)) { return $false }
  # Windows Terminal exposes WT_SESSION; supports truecolor since 2019.
  if ($env:WT_SESSION) { return $true }
  # COLORTERM=truecolor|24bit is the de-facto Unix signal — also honoured
  # by VSCode, kitty, alacritty, wezterm, iTerm2, Ghostty under PS.
  return ($env:COLORTERM -in 'truecolor','24bit')
}

if (Test-AnsiCapable) { Write-Output "`e[32mready`e[0m" }
Adaptive width — $Host.UI.RawUI.WindowSize.Width
# PS exposes terminal geometry through $Host.UI.RawUI. The Width is the
# column count of the visible window. Use it to size progress bars and
# tables that re-flow on resize — PS doesn't have a built-in resize event,
# so re-query at the top of each render loop.

function Get-Width {
  $w = $Host.UI.RawUI.WindowSize.Width
  if (-not $w -or $w -lt 1) { return 80 }  # piped / detached fallback
  return $w
}

function Render-Bar {
  param([int]$Pct)
  $w = [Math]::Max(10, (Get-Width) - 8)  # 8 for "100% [" + "]"
  $filled = [Math]::Round(($w * $Pct) / 100)
  return "{0,3}% [{1}{2}]" -f $Pct, ('█' * $filled), (' ' * ($w - $filled))
}

Write-Host (Render-Bar 42)
# Re-render on user-initiated tick / progress update — read width again.

Related sequences

Other languages