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
ConsoleColorvalues (Red,Yellow,DarkGray, …) and the host translates them into the appropriate output for the current sink. Trade-off:Write-Hostbypasses the output stream, so the coloured text is invisible to|,>, transcript logs, and4>&1redirects. - 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$PSStyleonly covers via the*Rgbstyle 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 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'
}# 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).# 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" }
# 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.