ANSI escape codes in Nim — std/terminal, \e, decimal \27
Nim's stdlib std/terminal is the most comprehensive batteries-included terminal module of any compiled language — a single import gives you SGR colour (setForegroundColor(fgRed)), text style (setStyle({styleBright, styleUnderscore})), 256-palette via Color(...), truecolor (with an explicit enableTrueColors() opt-in), cursor positioning, screen erase, terminal-size queries, raw-mode toggle, and isatty all in one place. For most apps the dependency list ends right there.
String-literal forms: Nim accepts "\e" (ESC named escape), "\x1b" (hex, exactly 2 digits), "\u001b" (4-digit Unicode), and "\u{1b}" (variable-width Unicode). The Nim twist — and the page's primary SERP differentiator — is that "\NNN" is decimal, not octal like every C-family language. "\27" in Nim is byte 27 (ESC); the same literal in C, Crystal, Python, or Ruby is byte \002 followed by the character '7'. If you're porting ANSI code from another language and "\033[31m" is throwing a parse error, the fix is "\27[31m" (decimal) or "\x1b[31m" (hex) or — most readably — "\e[31m".
For everything beyond std/terminal reach for: std/colors (stdlib colour-name table — colWhite, colAliceBlue, full HTML/CSS palette as Color constants, paired with enableTrueColors() to emit 38;2;R;G;B from the named constant); chronicles (status-im's structured logger — the canonical Nim logging library, ships ANSI colour topic / level / timestamp formatting out of the box, used by every Nim Ethereum/blockchain stack); illwill (johnnovak's curses-free TUI library — the modern Nim answer to ncurses, ships its own ANSI back end so it composes cleanly with std/terminal for non-fullscreen output).
Capability gating: isatty(stdout) from std/terminal returns false when stdout is redirected. Pair with getEnv("NO_COLOR") for the universal opt-out and getEnv("FORCE_COLOR") for opt-in. Windows VT mode: std/terminal calls SetConsoleMode with ENABLE_VIRTUAL_TERMINAL_PROCESSING automatically on first use of any colour helper, so you don't need a when defined(windows) gate around your colour calls — Nim handles it. Pre-Windows-10-1709 builds (Conhost without VT support) fall back to direct Win32 console-API colour calls, transparent to your code.
Recommended libraries
- std/terminal (stdlib)
The most batteries-included terminal stdlib of any compiled language. SGR (
setForegroundColor(fgRed)), style (setStyle({styleBright, styleUnderscore})), 256-palette + truecolor (afterenableTrueColors()), cursor (setCursorPos(x, y),cursorUp(n)), screen (eraseScreen,eraseLine), raw-mode (getch), terminal-size (terminalWidth(),terminalHeight(),terminalSize()), andisatty(stdout)— all in one import. Auto-enables Windows VT on first colour call.styledWrite(stdout, fgRed, "error")andstyledEcho(fgRed, styleBright, "error")are the canonical one-liners. - std/colors (stdlib)
Stdlib colour-name table — every HTML/CSS named colour as a
Colorconstant (colWhite,colAliceBlue,colCornflowerBlue, ...all 147 X11/CSS3 names). Combine withenableTrueColors()+setForegroundColor(stdout, Color(...))to emit38;2;R;G;Bfrom a readable name instead of raw RGB triples. Also exposesparseColor("#ff8000")for hex strings andmix(a, b, t)for interpolation — useful when generating gradient-coloured output. - chronicles
Status-im's structured logger — the canonical Nim logging library, used by nimbus-eth1/2, waku, libp2p, and most Nim Ethereum/blockchain stacks. Ships ANSI colouring for level (
debugcyan,infogreen,warnyellow,errorred,fatalmagenta), topic, timestamp, and key/value pairs out of the box. Compile-time topic filtering means zero runtime cost for disabled topics.info "started", port = 8080, ssl = trueproduces a coloured structured line on a TTY, plain JSON when redirected — capability auto-detect. - illwill
Curses-free TUI library — the modern Nim answer to ncurses. Pure Nim, no native dependency (talks to the terminal via raw ANSI bytes + termios on POSIX / SetConsoleMode on Windows). Provides
TerminalBuffer, double-buffered draw loop, keyboard polling with modifier keys, colour grids. Reach for it when you need a redrawing UI (file browsers, editors, dashboards) without the C-binding fragility of ncurses. Composes cleanly withstd/terminalfor non-fullscreen sections (e.g. status bar before entering full-screen mode).
Idiomatic patterns
# Nim accepts four ESC forms in "..." literals.
# Pick whichever your team reads fastest — all four
# compile to the same single byte (0x1b = 27).
#
# "\e" — ESC named escape (most readable)
# "\27" — \NNN is DECIMAL (the Nim twist!)
# "\x1b" — hex (exactly 2 digits)
# "\u001b" — Unicode 4-digit
#
# THE NIM TWIST: \NNN is DECIMAL — not octal like C,
# Crystal, Python, Ruby, etc. Nim's "\27" is byte 27.
# C's "\27" is byte \002 followed by character '7'.
# Porting from C? "\033" won't compile — use "\27"
# (decimal), "\x1b" (hex), or — best — "\e".
stdout.write "\e[1;31merror:\e[0m permission denied\n"
stdout.write "\27[33mwarn:\27[0m deprecated flag\n"
stdout.write "\x1b[32mok:\x1b[0m 142 tests passed\n"
stdout.write "\u001b[36minfo:\u001b[0m skipping cache\n"
# echo writes to stdout and adds a newline:
echo "\e[38;2;255;128;0morange truecolor\e[0m"
echo "\e[38;5;196m256-palette bright red\e[0m"
# Reusable helper — no stdlib needed:
proc esc(sgr, text: string): string =
"\e[" & sgr & "m" & text & "\e[0m"
echo esc("1;31", "FATAL"), " server crashed"
echo esc("32", "ok:"), " 142 tests passed"
echo esc("38;2;255;128;0", "truecolor orange")# Ships with every Nim install.
import std/terminal
# Basic SGR — fgRed/fgGreen/fgYellow/fgBlue/fgMagenta/fgCyan/fgWhite,
# bgRed/bgGreen/...; styleBright/styleDim/styleItalic/
# styleUnderscore/styleBlink/styleReverse/styleHidden/
# styleStrikethrough.
setForegroundColor(fgRed)
setStyle({styleBright})
stdout.write "error: "
resetAttributes()
echo "permission denied"
# styledEcho — one-liner, auto-reset, accepts an
# interleaved sequence of styles + strings:
styledEcho(fgGreen, styleBright, "ok: ", resetStyle, "142 tests passed")
styledEcho(fgYellow, "warn: ", resetStyle, "deprecated flag")
styledEcho(bgYellow, fgBlack, " CAUTION ", resetStyle, " brakes wet")
# 256-palette — Color() with a 0..255 index requires
# enableTrueColors() first (it switches stdout to the
# extended-color back end):
enableTrueColors()
for n in countup(0, 255, 16):
setForegroundColor(stdout, Color(n shl 16 or n shl 8 or n))
stdout.write n, " "
resetAttributes()
echo ""
# Truecolor — Color() built from 0xRRGGBB:
setForegroundColor(stdout, Color(0xFF8000)) # orange
echo "hot orange truecolor"
setForegroundColor(stdout, Color(0x006E82)) # deep teal
setStyle({styleBright})
echo "deep teal bold"
resetAttributes()
# Cursor + screen helpers — same module:
hideCursor()
setCursorPos(0, 0)
eraseScreen()
showCursor()# std/colors — 147 HTML/CSS named colours as Color
# constants; combine with std/terminal for readable output.
import std/[terminal, colors]
enableTrueColors()
# Named CSS colours as readable constants:
setForegroundColor(stdout, colCornflowerBlue)
echo "cornflower blue heading"
setForegroundColor(stdout, colTomato)
echo "tomato red error"
setForegroundColor(stdout, colMediumSeaGreen)
echo "medium sea green ok"
# parseColor() — read hex strings at runtime
# (e.g. from config files):
setForegroundColor(stdout, parseColor("#ff8000"))
echo "from hex string"
# mix() — interpolate between two colours,
# useful for gradient-coloured output:
let a = colRed
let b = colYellow
for t in 0..10:
setForegroundColor(stdout, mix(a, b, t.float / 10.0))
stdout.write "█"
resetAttributes()
echo ""
# ─── chronicles structured logging ───
# nimble install chronicles
import chronicles
# Compile-time topic filter — disabled topics cost zero
# at runtime. Enable via -d:chronicles_enabled_topics=...
logScope:
topics = "api"
service = "users"
info "request received", method = "GET", path = "/users/42"
warn "slow query", ms = 1240, table = "users"
error "auth failed", user_id = 42, reason = "expired_token"
# Output on a TTY:
# INF 2026-05-19 22:01:15.123 request received topics="api" ...
# Output piped to a file: plain JSON, no escapes.
# Auto-detection — no config needed.# Capability gating in Nim:
# isatty(stdout) — std/terminal, returns false when
# stdout is piped/redirected
# getEnv("NO_COLOR") — universal opt-out env var
# getEnv("FORCE_COLOR") — universal opt-in env var
#
# std/terminal auto-enables Windows VT on first colour call,
# so you do NOT need a "when defined(windows)" gate around
# your setForegroundColor calls — Nim handles it.
import std/[terminal, os]
proc ansiCapable(): bool =
if existsEnv("NO_COLOR"): return false
if existsEnv("FORCE_COLOR"): return true
isatty(stdout)
proc styled(text, sgr: string): string =
if ansiCapable(): "\e[" & sgr & "m" & text & "\e[0m"
else: text
echo styled("OK", "32")
echo styled("FAIL", "1;31")
# ─── illwill — pure-Nim TUI, no ncurses ───
# nimble install illwill
import illwill
proc exitProc() {.noconv.} =
illwillDeinit()
showCursor()
quit(0)
illwillInit(fullscreen = true)
setControlCHook(exitProc)
hideCursor()
var tb = newTerminalBuffer(terminalWidth(), terminalHeight())
while true:
tb.clear()
tb.write(2, 1, fgYellow, "illwill demo — q to quit",
resetStyle)
tb.write(2, 3, fgGreen, "✓ ok ", resetStyle, "142 tests passed")
tb.write(2, 4, fgRed, "✗ FAIL ", resetStyle, "3 tests failed")
tb.write(2, 5, fgCyan, "ℹ info ", resetStyle, "skipping cache")
tb.display()
let key = getKey()
case key
of Key.Q, Key.Escape: exitProc()
else: discard
sleep(20)