Lightweight ncurses alternative: ANSI terminal control, raw-mode key input, color, style, box drawing, and inline images (Kitty/iTerm2/sixel).
tur-ansi is a Tier 1 spice (pure Turmeric + inline-C, no cmake deps). It
covers the full surface of VT100/ANSI escape sequences you need to build
interactive terminal UIs without pulling in ncurses or a comparable library.
The spice is split into focused modules so you only import what you use:
| Module | What it provides |
|---|---|
ansi/term |
Terminal setup, capability detection, raw-mode key reading, SIGWINCH |
ansi/color |
4-bit (16), 8-bit (256), and 24-bit true-color SGR sequences |
ansi/style |
Text attributes: bold, italic, underline, blink, reverse, strikethrough |
ansi/cursor |
Absolute and relative cursor movement, show/hide, save/restore |
ansi/screen |
Screen and line clearing, alternate buffer, scrolling |
ansi/keys |
Key-name normalization, inverse mapping, modifier predicates |
ansi/box |
Unicode box-drawing glyphs, filled rectangles, titled boxes |
ansi/image |
Inline images: Kitty APC, iTerm2 OSC 1337, sixel, placeholder fallback |
:spices {
"ansi" {:url "https://github.com/rjungemann/turmeric-spices"
:ref "ansi-v0.1.3"
:subdir "spices/ansi"}
}
(import ansi/color :refer [fg24 bg24 color-reset])
(import ansi/style :refer [style-bold style-reset])
(style-bold)
(fg24 255 200 80)
(bg24 20 20 40)
(println "hello, tur-ansi")
(style-reset)
(color-reset)
(import ansi/term :refer [term-enable-raw term-disable-raw term-read-key])
(import ansi/keys :refer [key=?])
(term-enable-raw)
(let [k (term-read-key)]
(if (= (key=? k "<C-c>") 1)
(println "quit!")
(println k)))
(term-disable-raw)
(import ansi/screen :refer [screen-clear])
(import ansi/box :refer [box-fill box-draw box-title])
(screen-clear)
(box-fill 2 2 10 40 32) ;; clear interior with spaces
(box-draw 2 2 10 40 2) ;; style 2 = round corners
(box-title 2 2 40 " my panel ")
Terminal-wide setup and input. All other modules just emit escape sequences to
stdout; ansi/term owns the terminal state.
| Function | Signature | Description |
|---|---|---|
term-size |
() -> int |
Returns (cons rows cols); falls back to 24x80 |
term-color-support |
() -> int |
0=none 1=4-bit 2=8-bit 3=24-bit (env-detected) |
term-no-color? |
() -> int |
1 when $NO_COLOR is set and non-empty |
term-image-protocol |
() -> int |
0=none 1=kitty 2=iterm2 3=sixel |
term-enable-raw |
() -> void |
Enable raw mode + SIGWINCH self-pipe; idempotent |
term-disable-raw |
() -> void |
Restore cooked mode and tear down self-pipe; idempotent |
term-read-key |
() -> cstr |
Block until a keypress; returns a normalized key name |
term-read-key-timeout |
(ms: int) -> cstr |
Like term-read-key but returns "" after ms milliseconds |
term-on-resize |
(cb: ptr<void>) -> void |
Register a void(*)(void) callback for SIGWINCH; pass 0 to clear |
Key names follow the Neovim/Helix convention: printable chars are themselves
("a", "A", " "); non-printing keys are bracketed ("<Enter>",
"<Up>", "<C-a>", "<Resize>", "<EOF>").
The environment variable TUR_ANSI_TEST_KEYS bypasses select() and replays
newline-delimited key names from its value, enabling scripted tests without a
real tty.
SGR color sequences. Works in cooked or raw mode. The caller is responsible for
gating on (term-no-color?) when respecting the $NO_COLOR convention.
| Function | Signature | Description |
|---|---|---|
fg4 |
(idx: int) -> void |
4-bit foreground (0-15); clamped |
bg4 |
(idx: int) -> void |
4-bit background (0-15); clamped |
fg8 |
(idx: int) -> void |
8-bit (256-color) foreground; clamped to [0,255] |
bg8 |
(idx: int) -> void |
8-bit (256-color) background; clamped to [0,255] |
fg24 |
(r g b: int) -> void |
24-bit true-color foreground |
bg24 |
(r g b: int) -> void |
24-bit true-color background |
color-reset |
() -> void |
SGR 39;49 -- reset fg+bg only (preserves bold/italic etc.) |
Named index constants (return :int for use with fg4/bg4):
color-black, color-red, color-green, color-yellow, color-blue,
color-magenta, color-cyan, color-white, color-bright-black,
color-bright-red, color-bright-green, color-bright-yellow,
color-bright-blue, color-bright-magenta, color-bright-cyan,
color-bright-white.
(fg4 (color-bright-red)) ;; same as (fg4 9)
SGR attribute toggles. style-reset clears all attributes AND colors; use
color-reset when you only want to restore the default palette.
| Function | Signature | Description |
|---|---|---|
style-bold |
() -> void |
SGR 1 |
style-dim |
() -> void |
SGR 2 |
style-italic |
() -> void |
SGR 3 |
style-underline |
() -> void |
SGR 4 |
style-blink |
() -> void |
SGR 5 (slow blink) |
style-blink-fast |
() -> void |
SGR 6 (rapid blink; rarely supported) |
style-reverse |
() -> void |
SGR 7 (inverse video) |
style-strikethrough |
() -> void |
SGR 9 |
style-reset |
() -> void |
SGR 0 -- clears all attributes and colors |
VT100 cursor positioning. Works in cooked or raw mode.
| Function | Signature | Description |
|---|---|---|
cursor-move-to |
(row col: int) -> void |
Absolute 1-based position (CSI H) |
cursor-up |
(n: int) -> void |
Move up n rows |
cursor-down |
(n: int) -> void |
Move down n rows |
cursor-left |
(n: int) -> void |
Move left n columns |
cursor-right |
(n: int) -> void |
Move right n columns |
cursor-col |
(col: int) -> void |
Move to column on current row (CSI G) |
cursor-show |
() -> void |
DECTCEM h -- make cursor visible |
cursor-hide |
() -> void |
DECTCEM l -- hide cursor |
cursor-save |
() -> void |
DEC SC (ESC 7) -- save position |
cursor-restore |
() -> void |
DEC RC (ESC 8) -- restore saved position |
Screen and line clearing, alternate buffer, scrolling.
| Function | Signature | Description |
|---|---|---|
screen-clear |
() -> void |
CSI 2J + CSI H -- clear and home cursor |
screen-clear-to-eol |
() -> void |
CSI K -- erase from cursor to end of line |
screen-clear-line |
() -> void |
CSI 2K -- erase entire current line |
alt-screen-enter |
() -> void |
DECSET 1049 -- enter alternate screen buffer |
alt-screen-leave |
() -> void |
DECRST 1049 -- return to primary buffer |
scroll-up |
(n: int) -> void |
CSI S -- scroll viewport up n lines |
scroll-down |
(n: int) -> void |
CSI T -- scroll viewport down n lines |
Always pair alt-screen-enter with alt-screen-leave at shutdown to preserve
the user's scrollback history.
Key-name strings, byte parsing, and modifier predicates.
| Function | Signature | Description |
|---|---|---|
parse-key-bytes |
(bytes: cstr len: int) -> cstr |
Decode raw bytes into a normalized key name |
key-name->bytes |
(name: cstr) -> cstr |
Inverse: produce the byte sequence a terminal would send |
key=? |
(a b: cstr) -> int |
1 when two key names are equal |
key-ctrl? |
(k: cstr) -> int |
1 when key carries a C- modifier |
key-shift? |
(k: cstr) -> int |
1 when key carries an S- modifier |
key-alt? |
(k: cstr) -> int |
1 when key carries an M- (Meta/Alt) modifier |
Key name format: printable characters are passed through verbatim; others use
angle-bracket notation with optional modifier prefixes in order C-, M-, S-.
Examples: "a", "<Enter>", "<Up>", "<C-a>", "<S-Tab>",
"<C-M-S-Right>". Unknown byte sequences are returned as "<raw:HH...>" so
all input remains printable.
key-name->bytes is the inverse and is mainly useful for writing scripted
tests via TUR_ANSI_TEST_KEYS.
Unicode box-drawing characters and high-level rectangle helpers. Three border styles are available:
| Style int | Appearance |
|---|---|
0 |
Single line (+--+) |
1 |
Double line (+==+) |
2 |
Round corners (single lines with curved corners) |
| Function | Signature | Description |
|---|---|---|
box-draw |
(row col height width style: int) -> void |
Draw a bordered rectangle; minimum 2x2 |
box-fill |
(row col height width ch: int) -> void |
Fill the interior with ASCII char ch; call before box-draw |
box-title |
(row col width: int title: cstr) -> void |
Write a centered title on the top border; call after box-draw |
Individual glyph accessors (return :cstr): box-tl-single, box-tr-single,
box-bl-single, box-br-single, box-h-single, box-v-single,
box-tl-double, box-tr-double, box-bl-double, box-br-double,
box-h-double, box-v-double, box-tl-round, box-tr-round,
box-bl-round, box-br-round.
Typical draw order: box-fill first (so the fill does not overwrite the
border), then box-draw, then box-title.
Inline image display using the protocol best suited to the running terminal.
| Function | Signature | Description |
|---|---|---|
image-display |
(path: cstr) -> void |
Display a PNG file using the best detected protocol |
image-display-protocol |
(proto: int path: cstr) -> void |
Display with an explicit protocol (0-3) |
image-display-base64 |
(b64: cstr) -> void |
Emit a Kitty APC sequence from base64-encoded PNG data |
image-display-rgba |
(w h: int rgba: cstr) -> void |
Emit a sixel sequence from a raw RGBA pixel buffer |
image-placeholder |
(path: cstr) -> void |
Emit [image: path] as a text fallback |
Protocol codes match (term-image-protocol): 0=none/placeholder, 1=Kitty,
2=iTerm2, 3=sixel.
image-display-rgba uses a 6x6x6 color-cube quantizer (216 colors) and emits
run-length-encoded sixel bands. The caller must decode any compressed image
format (e.g. PNG via tur-png) into a flat RGBA byte buffer before calling.
Sixel support in image-display and image-display-protocol is deferred to
v0.2; those paths fall back to the placeholder for proto=3.
The examples/ directory has runnable demos:
| File | What it shows |
|---|---|
examples/hello-color.tur |
Capability detection, 256-color gradient, truecolor banner |
examples/keys-dump.tur |
Raw-mode key loop printing normalized key names as you type |
examples/box-demo.tur |
Three nested boxes (single/double/round) with titles |
examples/image-demo.tur |
Protocol detection and image-display / image-display-base64 |
tur run examples/hello-color.tur
tur run examples/keys-dump.tur # exit: Ctrl-C, Ctrl-D, or Esc Esc
tur run examples/box-demo.tur
IMAGE_DEMO_PATH=logo.png tur run examples/image-demo.tur
just test
Tests use TUR_ANSI_TEST_KEYS to feed scripted key sequences without a real
tty, so the key-reading and key-name test suites run safely in CI.