lua-nxterm is a feature-rich, pure-Lua drop-in replacement for ansicolors. On top of that you get 256-color and truecolor, fluent chaining, cursor and screen control, and modern terminal features (hyperlinks, titles, progress, synchronized output).
local colors = require('nxterm') -- was: require('ansicolors')
print(colors('%{bright red}Error:%{reset} something went wrong'))
print(colors.noReset('%{green}no resets around this'))local nxterm = require('nxterm')
local sc = nxterm.color
print(sc.red.bold('Error:') .. ' something went wrong')
print(sc'red bold ul' .. 'styled text')
print(nxterm.escape_codes('Status: %{green bold}OK%{}'))nxterm is a small, dependency-free toolkit for talking to terminals the way modern CLI tools expect:
- Colors & styles via SGR (standard 16 colors, bright, 256-color palette, truecolor RGB/hex, underline colors)
- Cursor & screen control (move, save/restore, hide, erase, scroll regions)
- Modern OSC/CSI extras (hyperlinks, window title, cwd, progress, synchronized output, focus, mouse)
- Ergonomic APIs — fluent chaining, classic key strings, and
%{...}embedding in format strings
It is designed to be easy to drop into existing projects, including as a drop-in replacement for ansicolors (see Migrating from ansicolors).
No ncurses. No C bindings. Just escape sequences that work on xterm-compatible terminals (Linux console, macOS Terminal, iTerm2, Windows Terminal, Kitty, Ghostty, WezTerm, Alacritty, tmux, …).
| Area | Capabilities |
|---|---|
| Styles | bold, dim, italic, underline (single/double/wavy/dot/dash), blink, inverse, strike, superscript/subscript |
| Colors | 16 ANSI + bright, 256-color (c88), greyscale (g0–g23), truecolor (r100g50b0, #rrggbb, #rgb) |
| Underline color | ul_c, ul_g, ul_r, ul_# where supported |
| Fluent API | sc.red.bold.ul emits one combined SGR sequence |
| Classic API | sc('red bold'), sc'red bold ul', sc['green italic'] |
| Embedding | %{red bold}text%{} via escape_codes / format / printf |
| Cursor | move, absolute/relative position, SCO + DEC save/restore, hide/show, style (DECSCUSR) |
| Screen | erase, line insert/delete, scroll up/down, scroll region |
| Modes | alt screen, bracketed paste, sync output (?2026), focus (?1004), wrap (?7) |
| Mouse | normal tracking + SGR reporting |
| OSC | title, hyperlinks (OSC 8), cwd (OSC 7), progress bar (OSC 9;4), bell |
| Environment | respects NO_COLOR; runtime nxterm.strip = true |
With LuaRocks:
luarocks install lua-nxtermFrom source:
git clone https://github.com/SamOrlando/lua-nxterm.git
cd lua-nxterm
luarocks make rockspecs/lua-nxterm-0.3.1-1.rockspecOr vendor the single file nxterm.lua next to your script:
your-app/
nxterm.lua
main.lua
local nxterm = require('nxterm')Pure Lua 5.1 – 5.4 and LuaJIT, no dependencies. Optional: Unix stty for fast terminal size detection and tty.raw() / tty.sane().
Runnable examples live in examples/ (lua examples/basic.lua, lua examples/showcase.lua for an animated truecolor demo). Every public function is documented in docs/reference.md.
local nxterm = require('nxterm')
local sc = nxterm.color -- or nxterm.colour
-- Fluent chaining (single SGR sequence)
print(sc.red.bold('Fatal') .. ' cannot continue')
-- Classic key string
print(sc('green bold') .. 'done' .. sc'')
-- Embedded codes (always starts and ends with reset)
print(nxterm.escape_codes('Build %{cyan}%{bold}passed%{} in 1.2s'))
-- Cursor + clear
io.write(nxterm.cursor.hide())
io.write(nxterm.erase()) -- clear screen
io.write(nxterm.cursor.setlc(1, 1))
io.write(nxterm.cursor.show())local sc = nxterm.color
-- 1) Fluent / chained properties
print(sc.red.bold.ul .. 'hello' .. sc.reset)
-- 2) Call with key list
print(sc('red bold ul') .. 'hello' .. sc'')
print(sc'red bold ul' .. 'hello') -- sugar
-- 3) Embedded in a string
print(nxterm.escape_codes('%{red bold ul}hello%{}'))Fluent chains accumulate parameters and emit one sequence:
sc.red.bold → ESC[31;1m (not ESC[31mESC[1m)
Calling a fluent chain with text auto-resets afterward:
print(sc.red.bold('hello')) -- ESC[31;1mhelloESC[0m| Kind | Examples |
|---|---|
| Foreground | black red green yellow blue magenta cyan white default |
| Bright fg | red_b green_b … grey / gray |
| Background | bg_red bg_blue … bg_default |
| Bright bg | bg_red_b … bg_grey |
| 256-color | c88 bg_c88 ul_c88 |
| Greyscale | g0…g23 bg_g10 ul_g10 |
| Truecolor | r200g100b20 bg_r100g200b255 ul_r0g0b0 |
| Hex | #BB0066 #f0c bg_#003366 ul_#ff0 |
Unknown or out-of-range keys (c256, g24, #12345, r300g0b0) are ignored rather than raising errors (ansicolors raises Unknown key instead; this is the one behavioural difference).
ansicolors aliases are accepted everywhere: hidden, blackbg … whitebg (kikito) and clear, underscore, onblack … onwhite (hoelzro).
bold / b, dim, italic / i, underline / ul / under,
ul_single ul_double ul_wavy ul_dot ul_dash,
blink, inverse / reverse, strike, hide,
sup / super, sub, and matching *_off / reset / rf / normal.
local es = nxterm.escape_codes
-- Each result is wrapped with reset so styles do not leak
print(es('[%{red}ERROR%{}] %{bold}disk full'))
-- empty %{} is also a reset
print(es('%{green}ok%{} continuing'))
-- format / printf process %{} then call string.format
nxterm.printf('Task %{cyan}%s%{} → %{green bold}%s', 'build', 'done')escape_strip(...) removes %{...} markers without emitting codes (useful for plain-text logs).
The nxterm module itself is a drop-in replacement for ansicolors.lua 1.0.2:
-- Before
local colors = require('ansicolors')
-- After
local colors = require('nxterm')
print(colors('%{red}hello')) -- ESC[0m ESC[31m hello ESC[0m
print(colors('%{bright blue redbg}x%{reset} y'))
print(colors.noReset('%{green}no reset around this'))colors(...)isnxterm.escape_codes(...): markup expanded and wrapped in resets; non-strings go throughtostring(nil/false→'').colors.noReset(str)expands markup only (strings and numbers).- All ansicolors keys work:
reset bright dim underline blink reverse hidden, the 8 colours andblackbg…whitebg;%{bright_red}=bright+red. - Every nxterm key works inside the markup too (
%{bg_c88},%{#ff8800},%{italic}), andNO_COLOR/nxterm.stripturn all codes off.
tests/test_ansicolors_compat.lua compares 1,167 cases with the real module. The output renders identically; the bytes can differ because:
- nxterm merges keys into one sequence (
ESC[1;31minstead ofESC[1mESC[31m); - an empty
%{}expands toESC[m(a reset) instead of nothing.
Differences in behaviour:
- Unknown keys are ignored, not raised as
Unknown key: <word>(the one documented difference). - An unterminated
%{stays literal text; in ansicolors it swallows the trailing reset. - ansicolors turns codes off on Windows unless
ANSICONis set; nxterm usesNO_COLOR/nxterm.strip.
To keep the old module name, add a one-line ansicolors.lua to your project:
return require('nxterm')hoelzro/ansicolors uses property access; nxterm.color supports the same forms and all of its key names (clear, underscore, hidden, onred …):
local colors = require('nxterm').color -- was: require('ansicolors')
print(colors.red .. 'hello' .. colors.reset)
print(colors.red('hello')) -- auto-reset after text
print(colors.red, 'hello', colors.reset)
print(colors.onblue .. colors.underscore .. 'x' .. colors.clear)
-- nxterm extras
print(colors.red.bold.ul('hello')) -- single combined sequence
print(colors('red bold underline') .. 'hi')
print(colors['bg_blue white bold'] .. 'banner' .. colors.reset)local c = nxterm.cursor
io.write(c.hide())
io.write(c.setlc(1, 1)) -- row 1, col 1 (standard order)
io.write(c.set(10, 5)) -- col 10, row 5
io.write(c.up(2) .. c.forward(4))
io.write(c.save()) -- SCO save
-- ...
io.write(c.restore())
io.write(c.dec_save()) -- DEC ESC 7 (often more reliable)
io.write(c.dec_restore()) -- DEC ESC 8
io.write(c.style(2)) -- DECSCUSR: 0 block, 1/2 block, 3/4 underline, 5/6 bar
io.write(c.show())Aliases: c.right / c.left / c.next_line / c.prev_line.
c.get() queries position via DSR (CSI 6n) and returns {col, row}, or nil on failure.
Note: this read can block if stdin is not a live TTY; prefer nxterm.size() for dimensions.
io.write(nxterm.erase()) -- clear screen (CSI 2J)
io.write(nxterm.erase(0)) -- clear below cursor
io.write(nxterm.line.erase()) -- clear line
io.write(nxterm.line.insert(2))
io.write(nxterm.scroll.up(3))
io.write(nxterm.scroll.region(2, 20)) -- set scrolling region
io.write(nxterm.scroll.region()) -- reset region (full screen)nxterm.clear is an alias of nxterm.erase.
io.write(nxterm.mode.alt_screen(true)) -- alternate buffer
io.write(nxterm.mode.bracketed_paste(true))
io.write(nxterm.mode.sync(true)) -- synchronized update (tear-free)
io.write(nxterm.mode.focus(true)) -- focus in/out events
io.write(nxterm.mode.wrap(false)) -- disable line wrap
io.write(nxterm.mouse.on()) -- ?1000 + ?1006
-- ...
io.write(nxterm.mouse.off())
io.write(nxterm.mode.alt_screen(false))Generic setters:
nxterm.mode.set(7, true) -- CSI ?7h
nxterm.mode.reset(7, true) -- CSI ?7l
nxterm.mode.set({1049, 2004}, true) -- several modes at once: CSI ?1049;2004hLegacy helpers: nxterm.mode.ansi() (VT52 → ANSI mode, ESC <) and nxterm.mode.gpu(on) (DEC special-graphics line-drawing charset on/off, ESC ( 0 / ESC ( B).
io.write(nxterm.title('my app'))
io.write(nxterm.cwd('/home/user/project'))
io.write(nxterm.bell())
-- Hyperlink (OSC 8); stripped to plain text when nxterm.strip is set
print(nxterm.link('https://example.com', 'example.com'))
-- Taskbar / tab progress (OSC 9;4)
-- state: 0=hide, 1=normal, 2=error, 3=indeterminate, 4=paused
io.write(nxterm.progress(1, 42)) -- 42%
io.write(nxterm.progress(3)) -- indeterminate
io.write(nxterm.progress(0)) -- hidelocal sz = nxterm.size() -- {width, height} or nil
if sz then
print(('cols=%d rows=%d'):format(sz[1], sz[2]))
endUses stty size when available, then falls back to a cursor probe.
nxterm.mode.size is an alias.
-- Automatic: strip defaults to true when NO_COLOR is set in the environment
-- Manual:
nxterm.strip = true -- all SGR / link sequences become empty / plain text
nxterm.strip = falseWhen stripped:
sc.red→''nxterm.link(url, text)→text%{red}→ empty substitution
nxterm
├── color / colour / sgr() fluent + classic SGR
├── nxterm(...) / noReset ansicolors drop-in (module call = escape_codes)
├── escape_codes / escape_strip
├── format / writef / printf / print
├── title, link, cwd, progress, bell
├── reset, erase / clear
├── line.{erase, insert, delete}
├── scroll.{up, down, region}
├── shift.{left, right}
├── char.{insert, delete, erase, rep}
├── tab, backtab
├── mode.{set, reset, alt_screen, bracketed_paste, sync, focus, wrap, ansi, gpu, size}
├── mouse.{on, off}
├── tty.{sane, raw}
├── cursor.{up, down, forward, back, nl, pl, set, setlc, setx, sety,
│ save, restore, dec_save, dec_restore, hide, show, style, get, getlc}
└── size()
local sc = nxterm.color
local function status(ok, msg)
local tag = ok and sc.green.bold('[ OK ]') or sc.red.bold('[FAIL]')
print(tag .. ' ' .. msg)
end
status(true, 'compiled 12 packages')
status(false, 'missing dependency: libfoo')io.write(nxterm.title('installing…'))
for i = 0, 100, 10 do
io.write(nxterm.progress(1, i))
io.write(('\r%{cyan}Installing%%%{reset} %3d%%'):format(i)
:gsub('%%{(.-)}', function(k)
return require('nxterm').color(k)
end))
io.flush()
-- sleep…
end
io.write(nxterm.progress(0))
io.write(nxterm.title('done'))
print()local c = nxterm.cursor
io.write(nxterm.mode.alt_screen(true))
io.write(nxterm.mode.sync(true))
io.write(c.hide())
io.write(nxterm.erase())
io.write(c.setlc(1, 1) .. nxterm.color.bg_blue.white.bold(' My TUI ') .. nxterm.color'')
-- draw…
io.write(nxterm.mode.sync(false))
io.write(c.show())
io.write(nxterm.mode.alt_screen(false))-- ansicolors.lua
return require('nxterm')- Single SGR emission on fluent chains — fewer bytes, cleaner logs, better for multiplexers.
escape_codesalways brackets output with reset so accidental style leaks are rare.- Empty key list (
sc'',%{}) emitsESC[m(full attribute reset). - No dependencies — sequences only; you decide when to write them.
- MIT license — use it anywhere.
cursor.get()may block if the terminal does not answer DSR; usesize()for geometry when possible.- Truecolor, underline styles, OSC 8/9;4, and sync mode depend on terminal support (degrade safely on older emulators).
- Windows: works under Windows Terminal / ConPTY with VT processing enabled;
stty-based size needs a Unix-like environment or the cursor fallback.
lua tests/test.lua # any of lua5.1 … lua5.4, luajit
lua tests/test_ansicolors_compat.lua # comparison with ansicolors (luarocks install ansicolors)
luarocks test # via the rockspec- The module is the ansicolors drop-in:
require('nxterm')is callable (same asescape_codes) and hasnoReset. The separatenxterm.ansicolorstable from 0.3.0 is removed. escape_codesconverts non-string arguments withtostring(nil/false→'') instead of skipping them.%{bright_red}-style keys (parts joined by_) expand to each part when every part is a key.- Unknown keys are ignored everywhere (0.3.0's
nxterm.ansicolorsraised).
nxterm.ansicolors: drop-in replacement for kikito/ansicolors.lua (call form,noReset, strict keys, byte-identical output).- Key aliases from both ansicolors libraries:
hidden,blackbg…whitebg,clear,underscore,onblack…onwhite.
- First public release.
MIT — Copyright (c) 2024–2026 Sam Orlando. See LICENSE.
- ANSI escape code
- XTerm control sequences
- NO_COLOR
- ansicolors.lua and hoelzro/ansicolors (API inspiration for the
%{...}markup and the simple color surface)
