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 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.