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