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 (after `enableTrueColors()`), cursor (`setCursorPos(x, y)`, `cursorUp(n)`), screen (`eraseScreen`, `eraseLine`), raw-mode (`getch`), terminal-size (`terminalWidth()`, `terminalHeight()`, `terminalSize()`), and `isatty(stdout)` — all in one import. Auto-enables Windows VT on first colour call. `styledWrite(stdout, fgRed, "error")` and `styledEcho(fgRed, styleBright, "error")` are the canonical one-liners.
- std/colors (stdlib)
Stdlib colour-name table — every HTML/CSS named colour as a `Color` constant (`colWhite`, `colAliceBlue`, `colCornflowerBlue`, ...all 147 X11/CSS3 names). Combine with `enableTrueColors()` + `setForegroundColor(stdout, Color(...))` to emit `38;2;R;G;B` from a readable name instead of raw RGB triples. Also exposes `parseColor("#ff8000")` for hex strings and `mix(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 (`debug` cyan, `info` green, `warn` yellow, `error` red, `fatal` magenta), 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 = true` produces 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 with `std/terminal` for 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)