Embedded scripting
August 5, 2026 ยท View on GitHub
zmax embeds several scripting interpreters directly in the IDE binary, so you can evaluate scripts against the live buffer with no external process. Each language drives the editor through one uniform host API.
| Language | Command(s) | Interpreter | Platforms |
|---|---|---|---|
| Emacs Lisp | :elisp (:eval-expression, :el) | elisprs | all |
| Vimscript (VimL) | :vim (:viml, :vimscript) | vimlrs | all |
| AWK | :awk (:awk-filter) | awkrs | all |
| zsh | :zsh (:zshell) | zshrs | unix only |
| stryke | :stryke (:st) | strykelang | unix only |
| Ruby | :ruby (:rb) | rubylang | unix only |
| PHP | :php | phplang | unix only |
| Python | :python (:py) | pythonrs | unix only |
| JavaScript | :node (:js, :javascript) | node-js | unix only |
| arb | :arb (:arb-filter) | arblang | unix only |
| Tcl | :tcl (:tclsh) | tclrs | unix only |
| R | :rlang (:rscript) | rlang | unix only |
๐ก These are gated behind the
scriptingCargo feature, which is on by default. A build made with--no-default-features(see Building from source) omits all of them โ the commands below then report that scripting was not compiled in.
Commands
:elisp <code>โ evaluate an Emacs Lisp expression against the editor; the result is shown on the status line. A subset of the editor is exposed as elisp builtins (point/region, buffer access,message, running typable commands, etc.).:vim <code>โ evaluate Vimscript; captured:echooutput and the trailing expression value are shown. Globals and functions persist across calls.:awk <program>โ filter the current selection (or the whole buffer when there is no selection) through an AWK program, replacing it with the program's output as a single undo step.:zsh <command>โ run a command line in the embedded shell; its captured output is shown in a popup. Shell state (variables, functions,cwd) persists across calls. Note:cd/exportmutate the real editor process.:stryke <code>โ evaluate stryke (strykelang) source; capturedprint/say/printfoutput or the last expression's value is shown, and state persists across calls.:ruby <code>โ evaluate Ruby source; capturedputs/printoutput or the value'sinspectis shown.:php <code>โ evaluate PHP source (the<?phpopen tag is optional); capturedecho/printoutput is shown.:python <code>โ evaluate Python source; capturedprintoutput or the value'srepris shown.:node <code>โ evaluate JavaScript source; capturedconsole.logoutput or the value'sinspectis shown.:arb <program>โ filter the current selection (or the whole buffer when there is no selection) through an arb spec'sout { }pipeline, replacing it with the pipeline's output as a single undo step.:tcl <script>โ evaluate Tcl source; what the script printed is shown, or the value of its last command when it printed nothing. State (set,proc) persists across calls. The interpreter runs on its own thread with the large stack tclrs's nesting limit is sized against, so a deepprocrecursion cannot overflow the editor's stack. Expressions must be braced โexpr {$a + 1}, notexpr $a + 1โ which is what tclrs's compiler accepts today (and the idiomatic Tcl spelling anyway).:rlang <code>โ evaluate R source; R's own transcript (autoprint,print,cat) is shown. Named:rlangbecause:ris vim's:read.
Polyglot pipelines (:xpipe)
:xpipe filters each selection through a chain of the embedded languages,
in this process. Stages are separated by a whitespace-delimited |>:
:xpipe awk '{print \$2}' |> php 'echo strtoupper($stdin);' |> ruby 'stdin.reverse'
Nothing forks. :pipe spawns a shell per selection and moves the text through
pipe file descriptors; every :xpipe stage is a call into an interpreter that
is already linked into the binary, so an N-stage chain costs N function calls
rather than N fork+execve pairs. The whole chain lands as one undo step, and
it runs over every selection.
Each stage receives the previous stage's output bound to a variable named
stdin, spelled in that language's own syntax:
| Stage language | Binding |
|---|---|
awk, arb | the record stream โ these are line filters and take input natively |
ruby, python, node, rlang, tcl, elisp | stdin |
php, zsh, stryke | $stdin |
vim | g:stdin |
A stage's output is what that language's own : command would have shown: what
the program printed, or its last value when it printed nothing.
The binding is a real value on that language's own runtime โ a Ruby String, a
Python str, a zsh parameter, an R character vector, a VimL g: variable โ so
the text is data. Nothing is escaped into the program, and no quote,
backslash, $ or | in the buffer can change what a stage means.
A chain built only from the line filters (awk, arb) runs its selections
across worker threads once there is enough text to pay for them; every other
language keeps its interpreter in thread-local state (or needs the editor
context), so those chains run on the editor thread as before. Either way the
results land in selection order as one undo step.
:xpipe <chain>(:xp,:|>) โ replace each selection with the chain's output.:xpipe-to <chain>โ run the chain and discard the output.:xpipe-insert <chain>/:xpipe-append <chain>โ run the chain with no input and insert/append its output at each selection.
Notes:
- A bare
|is live syntax in most of these languages (awk'sprint | "cmd", ruby/JS block parameters, zsh pipelines), which is why the separator is|>. Write\|>for a literal one inside a stage. - A stage may be wrapped in single quotes out of shell habit โ they are stripped. Double quotes are program text and are left alone.
elispstages are pure text filters: unlike:elisp, they do not mirror the live buffer, because the pipeline writes the result itself.- Failures name the stage:
xpipe: stage 2/3 (ruby): โฆ.
REPL
SPC a r (or :repl [lang]) opens a full-screen REPL panel fronting all of the
embedded languages behind one read-eval-print loop:
- Enter evaluates, Alt-Enter inserts a newline.
- Tab / Shift-Tab cycle the active language.
- โ/โ or C-p/C-n browse per-language history.
- C-l clears the transcript, PgUp/PgDn scroll, Esc closes.
:repl awk (etc.) opens directly on a given language. Per-language input history
is persisted to ~/.zmax/repl-history.toml.
Startup scripts
At startup zmax loads these files from the config directory (~/.zmax/) if
they exist, best-effort (errors surface on the status line):
init.elโ evaluated as Emacs Lisp.init.vimโ evaluated as Vimscript.