Spice version 0.1.0 -- Literate
.tur.mdnotebooks with a static renderer and an interactive terminal TUI. Audience: Turmeric users who want a Jupyter-style workflow for exploratory code, reproducible analyses, and shareable HTML reports.A
.tur.mdfile is a strict superset of CommonMark: ordinary markdown that renders cleanly in GitHub / VS Code / Obsidian / pandoc, where fenced code blocks taggedturmericorsweet-expare executable cells. Pair withtur-framefor data loading,tur-plotfor figures, ortur-statsfor analysis.
This guide walks through the workflow:
.tur.mdIn your project's build.tur:
:spices #{
"notebook" #{:url "https://github.com/rjungemann/turmeric-spices"
:ref "notebook-v0.1.0"
:subdir "spices/notebook"}
}
Then:
tur fetch
tur install tur-notebook ;; puts `tur-nb` on $PATH
No external C dependencies -- the parser, renderer, and TUI are pure Turmeric plus a vendored libturi for in-process cell execution.
Scaffold a starter file:
tur nb new analysis.tur.md
Open it in any editor. The body looks like ordinary markdown -- prose, headings,
lists -- except that fenced turmeric blocks are cells:
# My Analysis
Some prose explaining what we're about to do.
```turmeric
(+ 1 2)
```
More prose.
```turmeric {id=greeting}
(println "hello, notebook")
```
Cells have optional attributes inside {...} on the fence line (Quarto
style). The most common:
| Attribute | Default | Meaning |
|---|---|---|
id |
auto (cell-1, cell-2, ...) |
Stable handle for --cell and TUI navigation |
eval |
true |
Set false to render without executing |
echo |
true |
Set false to hide the source in rendered output |
output |
true |
Set false to suppress the output block |
error |
halt |
continue records the error and proceeds |
cache |
false |
Cache by source hash under .turnb-cache/ |
depends |
(none) | Comma-separated cell ids this cell depends on |
image |
inline |
inline = base64 in rendered file; file = sibling PNG |
Cells tagged sweet-exp use sweet-expression syntax -- everything else
about the workflow is identical.
Render to markdown (the default):
tur nb render analysis.tur.md # writes analysis.md
Render to a standalone HTML page:
tur nb render analysis.tur.md --to html # writes analysis.html
tur nb export is a more discoverable alias for the same workflow:
tur nb export md analysis.tur.md
tur nb export html analysis.tur.md
tur nb export html analysis.tur.md --out site/ # write into a directory
tur nb export md analysis.tur.md --no-output # strip output blocks
tur nb export md analysis.tur.md --no-source # outputs only
For "I'm writing prose, just keep the rendered file fresh," watch mode re-renders on every save:
tur nb render analysis.tur.md --watch
Watch mode uses kqueue on macOS and inotify on Linux. Each re-render
starts with a fresh session -- predictability over warm caches. If you want
warm-cache exploration, use the TUI instead.
tur nb tui analysis.tur.md
The TUI is modal, in the Jupyter / vim style. In command mode, single keys navigate and re-run cells:
| Key | Action |
|---|---|
j / k |
Move focus down / up |
gg / G |
Jump to first / last cell |
Enter |
Re-run the focused cell |
Shift-Enter |
Run focused cell, then move to the next |
R |
Restart session and re-run all |
r |
Re-run from the focused cell onward |
e |
Edit the focused cell ($EDITOR) |
a / b |
Insert a new cell above / below |
dd / p |
Delete (yank) / paste a cell |
o |
Toggle output visibility |
s |
Save the file |
/, n, N |
Search across cell sources and outputs |
? |
Help overlay |
q |
Quit (prompts if dirty) |
Hitting e writes the focused cell to a temp file and spawns $EDITOR on
it; on exit, the cell source is replaced and the file marked dirty. The TUI
does not ship its own text editor -- you get the keybindings, theme, and
plugins you have already configured for vim / helix / nano / emacs / VS Code.
The interpreter session lives for the lifetime of the TUI process: definitions
made in one cell are visible in later ones, exactly like a Jupyter kernel.
R is the "clean slate" key when you want to verify a notebook runs from a
fresh session.
For cells that are slow to recompute (loading a large CSV, fitting a model), opt in to source-hash caching:
```turmeric {id=load-data cache=true}
(import frame/csv :refer [read-csv default-csv-opts])
(def iris (read-csv "iris.csv" (default-csv-opts)))
```
```turmeric {id=fit-model cache=true depends=load-data}
(def model (fit iris))
```
The cache key is SHA-256(cell-source + sorted-attrs + dependency-hashes).
Editing load-data busts every downstream cell that lists it in depends.
Pass --cache to enable the cache for render / export:
tur nb render analysis.tur.md --cache
The cache lives in .turnb-cache/ beside the source file -- add it to
.gitignore.
tur-notebook does not require any plotting library. To embed an image, a
cell writes a PNG and announces its path via the image hook:
(import notebook/image :refer [image-hook-record-path])
(import plot/core :refer [plot-write-png])
(plot-write-png renderers opts "iris-scatter.png")
(image-hook-record-path "iris-scatter.png")
The hook works via a stdout marker (__NB_IMG__: <path>) that the session
intercepts before display. Cells get the path back as part of their
cell-output.image-paths list, and the TUI / renderers do the right thing
with it:
<img src=...> link when image=file is set on the cell).
tag, so the rendered .md is self-contained.[image: path] placeholder; the file is still on
disk and openable from the shell.This is the same opt-in pattern tur-plot and tur-plutovg use: cells that
write PNGs can advertise them, but the spices themselves stay independent of
the notebook tooling.
Notebooks that use randomness (any cell calling into tur-stats's rng-*
or any PRNG) should pass an explicit seed. The notebook tooling does
not auto-seed -- doing so would make notebooks that look reproducible
silently non-reproducible the moment they are edited. Spell the seed in
user code:
(import stats/rng :refer [rng-make])
(def rng (rng-make 42))
For CI, the exec subcommand runs cells without writing output blocks back
to disk:
tur nb exec analysis.tur.md --all # run every cell, print outputs
tur nb exec analysis.tur.md --cell fit-model # run from this cell onward
A non-zero exit code means at least one cell errored, so this composes
cleanly with set -e in a CI script:
#!/bin/sh
set -e
for nb in notebooks/*.tur.md; do
tur nb exec "$nb" --all >/dev/null
done
Combine with deterministic seeds and you can diff notebook outputs in version control to catch regressions in numerical behavior.
The TUI's defaults live in notebook/keys.tur. Override them with a
file passed to --keybindings:
# ~/.turnb-keys
# one "key action" per line; # starts a comment
j cell-next
k cell-prev
<Enter> run-cell
e edit-cell
R restart-and-run-all
q quit
tur nb tui analysis.tur.md --keybindings ~/.turnb-keys
User bindings are merged onto the built-in defaults; only the actions you
list are overridden, so a minimal file is fine. The available actions are
documented in notebook/keys.tur (default-keybindings).
--no-color disables ANSI colors entirely -- useful for terminals that do
not handle 256-color escapes well, or for screen-readers.
[image: path] placeholder
in the TUI. Rendered HTML and markdown carry the image regardless.The parser scope and the included / deferred features are documented in
docs/notebook-spice-plan.md -- file an issue if a missing CommonMark feature
is blocking real notebook work.