without it the TUI works fully and the web target shows a one-line notice)
September 4, 2026 · View on GitHub
█████╗ ██████╗ ██████╗
██╔══██╗██╔══██╗██╔══██╗
███████║██████╔╝██████╔╝
██╔══██║██╔══██╗██╔══██╗
██║ ██║██║ ██║██████╔╝
╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝
[A TUI FOR EVERY PIPELINE]
"A pipeline dumps text at you; arb turns it into an interface."
arb — visualize and modify Unix pipelines. Pipe a stream in and arb spawns a
dynamic TUI or a served web page, built from a declarative,
Tcl/Tk-flavored spec. It is a jq/xpath/css/yq superset — every leg
machine-checked against its own reference tool, and the numbers are below — an
interactive megafilter/map over the live passthrough, and an original language
on the
fusevm bytecode VM + three-tier
Cranelift JIT — the same engine behind zshrs, stryke, rubylang, and elisp.
Read the Docs · Engineering Report · Reference · Language Spec
Table of Contents
- [0x00] Overview
- [0x01] Install
- [0x02] Usage
- [0x03] Design
- [0x04] Query Engine
- [0x05] Command Line
- [0x06] Architecture
- [0x07] Status & Roadmap
- [0x08] Documentation
- [0xFF] License
[0x00] OVERVIEW
A pipeline dumps text at you; arb turns it into an interface. Drop it into a pipe and a small declarative spec describes widgets, layout, a uniform query over any format, and interactive controls that feed back into the passthrough — so arb sits mid-pipe and shapes what the downstream consumer receives, not just displays it. Highlights:
- Pipe-native — terminal-invoked, pipe-driven. No daemon; the web target
spawns a local UI host on demand (like
textual serve), not a server you run. - Dual target — the same spec renders to a ratatui TUI or a served web page
- WebSocket (
arb --serve), the browser dashboard built from the sharedzgui-corecomponent toolkit.
- WebSocket (
- One query engine — a
jq/xpath/css/yqsuperset over JSON, XML, HTML, YAML, TOML and CSV, including a real XPath 1.0 engine (all 13 axes, the node tests, positional predicates, the 27-function core library). What the word superset covers on each leg is measured, not asserted — see Containment, per leg. - Megafilter/map — interactive controls render and feed
out, so a control's path used as a value is its current state — arb filters and maps the downstream output live. - fzf superset + orchestrator —
arb --fzfis a fuzzy select mode (rank, smart-case, fzf's extended query language, multi-select, preview);arb 'PROD | _ | CONS'runs a whole pipeline with arb as the_stage, hooking each command's fds so producer stderr lands in a pane instead of corrupting the TUI. - Runs on fusevm — the compute core (expressions and the
calcpipeline op) lowers to afusevm::Chunkand executes on the fusevm VM + three-tier Cranelift JIT. Declarative widget/layout construction needs no VM; more of the language moves onto fusevm as the expression layer grows. - Original, not a port — an original language in stryke's class, deliberately lean (rubylang-scale, not stryke-scale). It reuses mechanics from its siblings (fusevm embedding, the rkyv script cache, the LSP/DAP stdio shape, the package-manager ABI) but the lexer, parser, AST, lowering, and semantics are arb's own.
- World-first = synthesis + ecosystem — no single leg is new (Tcl/Tk, Expect,
dasel, ratatui,
textual serveare all prior art); the combination is: a pipe-native, dual-target, component-generating UI language with a shareable dashboard registry. No registry of installable pipeline TUIs exists today.
[0x01] INSTALL
# Via Homebrew tap (bumped by each release; formula is `arb`)
brew tap MenkeTechnologies/menketech
brew install arb
# Or from source (--recurse-submodules pulls zgui-core for the web dashboard;
# without it the TUI works fully and the web target shows a one-line notice)
git clone --recurse-submodules https://github.com/MenkeTechnologies/arb
cd arb
cargo build
find / | ./target/debug/arb
# Or via crates.io (the crate is `arblang` — the name `arb` was taken;
# it still installs the `arb` binary)
cargo install arblang
arb builds as a standalone Rust crate — a lib + bin, so the language front-end is
unit-testable without a terminal. Run the tests with cargo test.
[0x02] USAGE
# zero-config: a live tail of stdin with a count + rate header; q / Esc / Ctrl-C quits
find / | arb
# a spec: a gauge fed by the live line count of the stream
seq 1 100000 | arb -e 'gauge .g -max 100000; source .g { in; count }'
# a filtered list: keep 5xx lines, drop health checks
tail -f access.log | arb -e 'list .l; source .l { in; match /5\d\d/; reject /health/ }'
# pipe citizen — a viz tap/peek: arb sits mid-pipe, and with no `out { }` it
# passes the stream through untouched, so the downstream consumer still gets it
find / | arb dash.arb | stryke
# or a filter/map: an `out { }` block reshapes what flows downstream (streams
# live for per-line pipelines; `head`/consumer exit is a clean stop, not an error)
tail -f access.log | arb -e 'out { in; match /5\d\d/; field 7 }' | stryke
Mid-pipe, arb is either a tap/peek (no out — the stream passes through
unchanged while the spec visualizes it) or a filter/map (an out { … } block
reshapes the passthrough). The TUI renders to /dev/tty (like fzf), so stdout
stays a clean data channel; keys are read straight from /dev/tty (like vipe),
so find / | arb works even though stdin carries the pipe.
Interactive filter (megafilter)
In the TUI, type to filter the whole dashboard live (case-insensitive
substring); Bksp/Ctrl-U edit, Esc clears, Ctrl-C quits. When piped
onward, the filter also narrows what the downstream consumer receives — the
megafilter reshapes the pipe as you type.
Interactive map (megafilter/map)
The filter narrows; an out { … apply .x } pipeline fed by an input widget
maps — you edit a transform in the TUI and the downstream pipe updates live,
so arb is a scriptable, interactive stage in the middle of a pipeline:
tail -f access.log | arb -e 'input .x -placeholder "transform, e.g. field 7"
tail .t
source .t { in }
out { in; apply .x }' | downstream
# type `field 7` → downstream receives column 7 of every line, live
# type `grep /404/` → downstream receives only 404s; clear it → full stream again
The out map runs per line as the stream flows (never buffered — arb doesn't
block the pipe like vipe), re-resolving only when you edit the field. An empty
field is identity (the pipe passes through untouched until you type). A filtering
transform (grep/reject) drops lines downstream; a reducer (count) can't map
a single line and falls back to passthrough. The TUI stays up on /dev/tty while
stdout carries the mapped data.
Key bindings — bind C-<key> <action>
Drive the spec's own state from the keyboard. A bind maps a control key (so
it never shadows filter typing) to an action: set .name VALUE writes an input
value — with an out { … apply .name } map that reshapes the live pipe on a
keystroke — or quit:
tail -f access.log | arb -e 'input .x
tail .t
source .t { in }
out { in; apply .x }
bind C-u set .x upper # Ctrl-U: uppercase the pipe
bind C-e set .x "grep /ERROR/" # Ctrl-E: only errors
bind C-r set .x "" # Ctrl-R: reset to passthrough
bind C-q quit' | downstream
Keys are C-<letter> (also c-<letter> or ^<letter>). set binds turn the
interactive map into a set of one-key presets; quit exits.
Reactions — expect /regex/ <action>
The "react" half of Expect: when a stream line matches a pattern, fire an
action automatically — no keypress. Same action vocabulary as bind (set a
control, quit), so the stream can drive itself:
tail -f deploy.log | arb -e 'input .x
tail .t
source .t { in }
out { in; apply .x }
expect /ERROR/ set .x "grep /ERROR/" # errors appear → pipe narrows to them
expect /deploy ok/ quit' # success line → exit
Patterns are checked against new lines as they arrive (on the redraw cadence; a
line that scrolls past faster than a frame on a bounded dashboard may be missed).
Combined with bind, a spec reacts to both the keyboard and the stream — the
basis for spawn/expect/react automation.
fzf mode — arb --fzf
A fuzzy select mode: filter a stream and pick line(s), printed to stdout on Enter. A superset of fzf's core (fuzzy match + ranking + smart-case, multi-select, a preview pane), not a re-skin.
vim "$(git ls-files | arb --fzf)" # single select
ls *.log | arb --fzf # type to fuzzy-filter, Enter picks
-
Fuzzy match — fzf's own algorithm (
FuzzyMatchV2, ported from the Go source insrc/algo.rs): the same score matrix, boundary/camelCase bonuses, gap penalties and the packed four-slot rank key, so a query returns the same rankingfzfreturns, down to the line order.--schemeand--tiebreakreorder it the way they reorder fzf — including the scan direction and begin-offset backtrace fzf derives from the criteria rather than from a flag. Smart-case: a lowercase query is case-insensitive, any uppercase makes it case-sensitive. -
Extended search — fzf's query language, ported from the Go source (
pattern.go) insrc/pattern.rs, and on by default exactly as in fzf. A query is a list of terms; every term must match, and a sigil changes how one matches:term matches mainfuzzy 'mainexact substring 'main'exact, on a word boundary ^srcat the start of the line rs$at the end of the line ^main.rs$the whole line !testlines that do NOT contain it a | beither one (an OR set) my\ filea literal space, not a term break git ls-files | arb --fzf --filter '^src rs$ !test' # under src/, ends in rs, not a test git ls-files | arb --fzf --filter '^src | ^lib' # under either directory!,^and$compose (!^src,!rs$), and'flips exactness in both directions — under-e/--exacta'termis the FUZZY one.+x/--no-extendedturns the whole query back into a single literal fuzzy term;-x/--extendedis the default. -
-f/--filter STR— the matcher without the UI: print the ranked matches and exit (find / | arb --fzf --filter conf). The whole stream is read, split in one pass, then scored withrayonand ordered with a parallel sort (src/cli.rsrun_filter,src/tui.rsrank), so the ranking runs across every core — 1.26M lines in 123 ms, measured below. -
Navigate —
↑/↓,Ctrl-J/Ctrl-Ndown,Ctrl-K/Ctrl-Pup. -
Multi-select — with
-m/--multi,Tabmarks lines (┃in the mark column) and moves down; Enter emits all marked. -
Matched chars highlight; keeps the entire stream (no line drop), so marks persist and a huge
find /stays fully selectable.
Parity and throughput vs fzf and skim
The superset claim is a contract, so it is checked the way the jq/xpath/yq claim
is: scripts/fzf_bench.sh runs one corpus of probes through all three engines
and diffs stdout, then times the same invocations. arb is compared byte-exact
against fzf --filter — output ORDER is part of parity, not just the match set —
and set-exact against sk --filter, whose own scorer orders ties differently.
A probe whose reference REFUSES the invocation (a flag it does not define) skips
with the reason printed; there is no allowlist, and the exit status is the number
of divergences.
Recorded run — corpus of 1,259,869 real filesystem paths (127,414,004 bytes), arb 0.1.16 / fzf 0.74.3 / sk 4.6.0, Apple M5 Max (12P+6E), hyperfine 1.20.0:
Parity — 39 pass / 1 diverged / 2 skipped. Every fuzzy, exact, prefix,
suffix, negated, OR-set and multi-term query matched fzf byte-for-byte, as did
--exact, --no-sort, --tac, --algo=v1, --tiebreak=length|end,
--scheme=path, --ignore-case and smart-case. The two skips are skim flags
that do not exist (--algo=v1, --ignore-case). The one divergence is skim's
own defect, not arb's: sk --filter cargo --tac emits duplicates (8,103 lines,
2,123 unique, where its non---tac run answers 8,053 distinct) — arb and fzf
agree on that probe.
Throughput (hyperfine, mean ± σ; 8 runs per corpus query, 30 for startup):
| query | arb | fzf | skim | vs fzf | vs skim |
|---|---|---|---|---|---|
cargo (dense hit rate) | 123.1 ± 2.0 ms | 409.3 ± 19.8 ms | 232.2 ± 11.2 ms | 3.33× | 1.89× |
libcore | 96.4 ± 1.0 ms | 256.9 ± 4.4 ms | 177.8 ± 4.8 ms | 2.66× | 1.84× |
'cargo (exact) | 70.2 ± 0.7 ms | 200.3 ± 1.5 ms | 88.0 ± 3.8 ms | 2.85× | 1.25× |
^/opt .rs$ !zsh | 49.4 ± 0.8 ms | 78.2 ± 1.0 ms | 70.4 ± 2.5 ms | 1.58× | 1.42× |
zqxjvw (no match) | 48.8 ± 0.6 ms | 67.3 ± 0.7 ms | 61.8 ± 12.4 ms | 1.38× | 1.26× |
| startup, empty stdin | 4.1 ± 0.4 ms | 3.2 ± 0.4 ms | 5.3 ± 0.9 ms | 0.78× | 1.29× |
That is ~1.0 GB/s of candidates on the cargo row, against ~0.31 GB/s for fzf
and ~0.55 GB/s for skim. The spread is the matcher, not I/O: with a query that
matches nothing, every engine is mostly reading stdin and the gap narrows to
1.38×. Startup is the one axis arb loses — fzf's static Go binary costs ~1 ms
less to launch. arb is ahead again by the time there is a corpus to score: on a
10,000-line slice of the same paths, cargo takes 6.1 ± 0.5 ms in arb against
7.9 ± 0.4 ms in fzf and 10.3 ± 0.6 ms in skim.
Drop-in for fzf. arb --fzf reads your fzf configuration the way the fzf
binary does — $FZF_DEFAULT_OPTS_FILE, then $FZF_DEFAULT_OPTS, then the
command line (later wins) — and renders the picker to match: same layout, border,
separator, pointer/marker glyphs, palette, scrollbar, ellipsis and ranking. Point
a wrapper at it (e.g. ZPWR_FZF='arb --fzf') and nothing else changes.
Honored: --layout/--reverse, --border[=STYLE], --info=STYLE, --color
(dark/light/16/bw plus per-slot overrides), --pointer, --marker,
--ellipsis, --scrollbar/--no-scrollbar, --scroll-off, --cycle, --tac,
--ansi (the input's own colours render; matching and output use the text
without the codes), -d/--delimiter + -n/--nth + --with-nth (field
selection, fzf's tokenizer), --header-lines, --print-query, --expect
(fzf's output contract: query line, accepting key, then the selection — what
fzf-tab parses), --tiebreak (all six criteria: length, index, begin,
end, chunk, pathname), --scheme (default/path/history), --algo
(v2/v1), --read0 + --print0 (NUL record framing), --accept-nth (print
fields, or a {n} template, instead of the line), --tail=N, --bind (up, down, page-up,
page-down, half-page-up, half-page-down, first, last, toggle,
toggle-all, accept, abort, clear-query, backward-delete-char,
ignore, and +-chains like tab:toggle+down), -e/--exact,
-x/--extended + +x/--no-extended (extended is the default, as in fzf),
--no-sort/--sort,
-i/--ignore-case + +i/--no-ignore-case + --smart-case (smart-case is the
default, as in fzf), -q/--query, -m/--multi[=MAX], -f/--filter, --prompt, --header, --height
(100% means full screen and runs on the alternate screen, as in fzf, so
quitting restores the terminal; anything smaller draws below the cursor,
scrolling to make room, with fzf's --min-height=10+ floor on percentages),
--preview 'CMD {}' (run with $SHELL -c, or --with-shell, so a preview
written against your shell works). A binding naming an action arb has no equivalent for
(execute(…), preview control, …) is skipped so the key keeps its built-in
behavior; the remaining fzf flags are accepted and ignored so the command still
runs.
Every fzf 0.74 option parses, negations included: --no-X cancels an earlier
--X exactly as fzf's does, so --preview CMD --no-preview ends up with no
preview rather than aborting the run. That is what _fzf_complete_kill sends
(-m --header-lines=1 --no-preview --wrap), and what a --no-prefixed flag in
$FZF_DEFAULT_OPTS sends on every single call. An option whose value is
optional follows fzf's rule for it too — --border STYLE takes the next word,
--border --preview CMD does not, and --gap N takes it only when it is a
number — so the flag behind it is never swallowed.
The picker uses fzf's own palette, not arb's theme, unless a theme is asked for
explicitly (--theme NAME, a spec theme directive, or the live Ctrl-T
chooser) — the point of the drop-in is that it looks like fzf.
--fzf is a DSL spec, not a hardcoded mode. It synthesizes a one-widget
select spec — so the select surface is expressible directly, and -prompt/
-header become widget opts:
git ls-files | arb --fzf # sugar for the spec below
git ls-files | arb -e 'select .files -prompt "pick> " -header files
source .files { in }' # identical: fzf as a spec
A select widget anywhere in a spec puts the TUI in select mode, so fzf is just
one shape the DSL can build — the same DSL that builds dashboards and the
input/apply transform editor above.
Projected candidates (--with-nth/--nth). The select widget's source
pipeline transforms what's shown and searched, while Enter still emits the
original line — so you pick from a clean view and get the raw record:
ps aux | arb -e 'select .p { in; field 11 }' # search/show the command column,
# Enter emits the whole ps row
git log --oneline | arb -e 'select .c { in; grep /fix/ }' # candidates pre-filtered
The projection is per-line: field, upper, grep, extract, … map a line to
its display row(s); a filtering verb drops non-matches from the list. Cross-line
verbs (sort, count) can't project and fall back to identity.
Search a key, show the whole line (--nth). A search .name { … } binding
derives the fuzzy-match key per line while the row still shows and emits the full
source display — so you keep every column in view but type against just one:
ps aux | arb -e 'select .p
source .p { in } # show the whole ps row
search .p { in; field 11 }' # but fuzzy-match only the command
search is pipeline-general (match a lowercased key, an extracted field, a regex
capture), not just a column index. Omit it and the search key is the display.
Pipeline orchestrator — arb '<PROD> | _ | <CONS>'
arb runs a whole pipeline with _ marking its own interactive stage, so it owns
every stage's file descriptors. The producer's stderr goes to a pane instead
of corrupting the TUI (the reason plain find / | fzf gets scribbled over by
permission errors):
arb --fzf 'sudo find / | _ | perl -pe "s|Application|APP|"'
# └ producer ┘ │ └──────── consumer ────────┘
# stdout→list │ selection piped through it on Enter
# stderr→⚠ pane arb's interactive stage
Each stage is shelled out (sh -c, so globs/quotes work); arb wires the
fds between them. (--run 'PIPELINE' is the explicit-flag form.)
Interactive editor — input widget + apply verb
fzf is one TUI. The DSL builds arbitrary ones. An input .name widget is a live
editable field; the apply .name verb splices its current value into a source
pipeline, re-evaluated every frame. That makes a before/after transform editor
a spec, not a mode:
printf 'alice\nbob\ncarol\n' | arb -e '
input .q -placeholder "transform (e.g. upper)"
list .before
list .after
source .before { in }
source .after { in; apply .q }'
# type `upper` in the field → the .after pane recomputes `in; upper` live
Tab cycles focus between fields, typing edits the focused one, Esc/Ctrl-U
clear it. Any query verb (upper, field N, grep /re/, commafy, …) is valid
after apply, so the field drives the whole downstream pipeline interactively.
Web dashboard — arb --serve
The same spec that drives the ratatui TUI drives a browser. --serve starts a
local HTTP server (std-only, no framework), serves one self-contained page, and
the page polls the live stream — so a pipeline becomes a shareable dashboard:
tail -f metrics.log | arb --serve --port 8787 -e 'gauge .rps -max 1000
source .rps { in; rate }
histo .codes
source .codes { in; field 9; tally }'
# → arb: serving dashboard at http://127.0.0.1:8787/
The page is built with zgui-core
— the shared cyberpunk web-component toolkit, vendored as a git submodule at
lib/zgui-core and bundled into the binary at build time (build.rs →
include_str!, so the binary stays self-contained). It mounts ZGui.appShell
(splash, filter bar, ⌘K palette, settings/colorscheme) and renders each widget
with the matching component — gauge→ZGui.gauge, chart→ZGui.chart,
spark→ZGui.sparkline, bars/histo→ZGui.statBars, table→ZGui.dataTable,
containers/log→ZGui.card+ZGui.logView. Every widget's source is evaluated
server-side and pushed as structured JSON; the client feeds it to the component
handles (.set/.setSeries/.setRows) — never innerHTML with stream data, so
nothing can inject markup. the input/filter fields, slider (range), check (checkbox), and facet
(multi-select, with -field candidates computed server-side) controls render as
real form elements that POST /set on change, so the server re-resolves the
bound pipelines live — the browser drives the megafilter/map just like the
terminal. --port 0 picks a free port and prints it.
The web target needs the submodule checked out:
git submodule update --init. Without it the binary still builds (the dashboard shows a one-line notice).
Updates arrive over a WebSocket (/ws) — the server pushes a frame every
250 ms, no polling lag. The handshake and framing are hand-rolled over the same
std TCP socket (SHA-1 + base64, no crypto or WebSocket dependency); if the
browser or connection can't upgrade, the client automatically falls back to
polling /data.
Presets & sharing — --save / --install
A spec is a portable file, so dashboards are shareable units. Save your own, install ones others send you, and run any of them by name from anywhere:
arb --save api -e 'gauge .g -max 1000; source .g { in; rate }' # save your own
arb --install team-dash.arb # install a shared spec
arb --install team-dash.arb --as prod # …under a chosen name
arb --installed # list installed presets
find / | arb -p api # run one by name
arb --uninstall api # remove it
Installed specs live in ~/.arb/lib (override with $ARB_LIB); the first #
comment line is the description shown by --installed/--list, and the second
is the invocation that feeds it — printed under the description there, and again
if you run the spec with nothing piped in. Install
validates the spec before adding it, so the library only holds runnable
dashboards. A shared spec is any .arb file today; a remote registry (install by
URL/name) plugs into the same resolver next.
Worked examples — examples/
The examples/ directory holds small, self-contained dashboards
that each demonstrate one idiom, with the exact producer in the header comment.
Run any of them with -f:
tail -f app.log | arb examples/error-rate.arb # errors vs. all, as a gauge + tail
tail -f access.logfmt | arb examples/http-status.arb # status-code bars + 5xx counter (logfmt)
awk '{print \$1}' log | arb examples/top-talkers.arb # rank busiest clients (columnar)
cat requests.jsonl | arb examples/json-latency.arb # avg/peak ms + slow-request tail (JSON)
cat prose.txt | arb examples/word-freq.arb # tokenize + rank words
df -Pk | arb examples/disk-usage.arb # mount table + filesystem count
Every example is covered by tests/examples.rs: each parses and builds, and the
named ones have their source { … } pipelines evaluated against sample input
and asserted, so the examples are proven to compute what their comment claims.
[0x03] DESIGN
| Piece | How |
|---|---|
| Pipe-native | Terminal-invoked, pipe-driven. No daemon; the web target spawns a local UI host on demand (like textual serve), not a server you run. |
| Tcl/Tk-flavored, not Tcl | Commands take args and verbatim { } blocks; widget paths are dot-hierarchical (.a.b.c). No $, [cmd], or expr{} substitution. |
| One query engine | A single vocabulary works uniformly over JSON, XML, HTML, YAML, TOML, and CSV: a jq/xpath/css/yq superset, each leg machine-checked against its own reference tool. Containment per leg is measured — see Containment, per leg. |
| Megafilter/map | Interactive controls render and feed out, so a control's path used as a value is its current state — arb filters and maps the downstream output live. |
| Runs on fusevm | The computational core — expressions and the calc pipeline op — lowers to a fusevm::Chunk and executes on the fusevm VM (three-tier Cranelift JIT). Declarative widget/layout construction needs no VM; more of the language moves onto fusevm as the expression layer grows. |
The full grammar — values, variables, functions, widgets, layout, controls,
Expect reactions, actors, modules, and the package manager — is in
SPEC.md.
[0x04] QUERY ENGINE
A single query vocabulary works uniformly over every format — a jq/xpath/
css/yq vocabulary. You can write the arb-native verbs, or paste the jq /
xpath literal directly (it compiles to the same ops):
out { in.json; .users[] | select(.age >= 18) | .name } # jq literal
out { in.html; //a/@href } # xpath literal
| jq / xpath / css | arb-native |
|---|---|
.users[].name | field users; each; field name |
.items[] | select(.price>10) | field items; each; where(price>10) |
{name, age} (projection) | pick name age |
//a/@href | find a; attr href |
div.card h2 | sel {div.card h2} |
arb implements jq, not a subset of it. The comma operator, object and array
construction, if/elif, try/catch and ?, //, .., as bindings with
destructuring and ?//, reduce/foreach, label/break, def (with filter
and value parameters, and recursion), string interpolation and every @format,
the whole assignment family, the path builtins, the regex family and the stream
builtins all answer exactly as jq does. The xpath front-end is still a documented
subset: anything outside it is a hard error (xpath: …), never silently
guessed. So is a TYPE mismatch on either side — null | .[], true | length,
{"a":1} | . + 3 and .n / 0 all refuse and exit non-zero, because jq refuses
them too and an answer where the reference raises is the same silent guess.
Two things about the value model are observable and both match jq. Object keys
keep INSERTION order, so keys_unsorted and to_entries report the document's
own order while keys sorts. And a number keeps the source LITERAL it was read
with until arithmetic touches it, printed in decNumber's canonical form: 1.50
stays 1.50, 1e2 prints as 1E+2, 12345678901234567890 round-trips, and
. + 0 on any of them collapses to the double. A top-level JSON string renders
RAW, as jq -r prints one — a line reading "hello" is hello.
All of that is checked by scripts/jq_parity.sh, which runs arb and the
reference tool over one corpus and byte-diffs stdout. Six probe kinds:
jq_probe must match jq -rc, xp_probe must match xmllint --xpath,
yq_probe must match yq -o=json -I=0, type_probe requires BOTH engines to
refuse and verifies that jq really does, text_probe covers the non-JSON line
where jq refuses the input outright and there is no oracle at all, and
ext_probe covers a builtin arb keeps that jq 1.8 dropped. Three more are the
containment the word superset actually names, one per reference:
superset_probe requires every name/arity in jq's own builtins to exist in
arb's, xpath_superset_probe every XPath 1.0 axis, core function and node test,
and yq_superset_probe every yq operator. Each asks the REFERENCE first, so arb
is never charged for a name the reference does not define either. There is no
allowlist.
One deviation runs the other way and is deliberate: for an integer above ,
jq's own arithmetic loses up to an ULP (jq answers true to
(-516424571754902561 + 0) == -516424571754902500 when the correctly-rounded
double ends …600). arb reads with Rust's correctly-rounded parser and prints
the shortest decimal that round-trips, so it differs from the reference by being
right; tests/jqlang.rs states that tolerance explicitly and byte-matches jq
everywhere else.
Containment, per leg
The containment probes are what turn the word superset into a number, one per reference. Current run — 825 probes, 825 pass, 0 diverged, no allowlist:
| leg | reference | containment | status |
|---|---|---|---|
| jq | jq 1.8.2 builtins | every name present | superset, machine-checked |
| xpath | XPath 1.0 (W3C REC) via xmllint | 48 of 48 enumerated constructs present | superset of the enumerated surface |
| css | Selectors 3/4 via scraper | 24 selection probes match the equivalent XPath | matches the reference on every probe |
| yq | mikefarah/yq v4.53.6 | every enumerated operator present | superset of the enumerated surface |
XPath. src/xpath.rs used to compile XPath-shaped syntax to a CSS selector,
which could not carry an axis or a function at all — the probe found 46 of 48
constructs missing. It is a real engine now: xpath_syntax.rs lexes and
parses the full grammar of the W3C Recommendation and xpath_eval.rs evaluates
it over the parsed document. All 13 axes, the four node tests (node(),
text(), comment(), processing-instruction()) plus name and wildcard tests,
predicates with correct proximity positions — including the rule that a
predicate on a REVERSE axis counts along that axis, so preceding-sibling::p[1]
is the nearest one — and the 27-function core library. 59 xp_probes
byte-diff it against xmllint --html --xpath on the same document.
Four of those were worse than missing, and they are the reason this was the
priority: SPEC §8 promises anything outside the subset is "a hard error … never
silently reinterpreted", and for these it was not. [@a='x' or @a='y'] and a
chained predicate [@a='x'][@b='y'] exited 0 with an EMPTY selection where
XPath selects nodes; a rooted path (/li/text(), /div/h2/text()) exited 0
with a NON-EMPTY node set where XPath selects nothing, because a leading
/step was compiled as a descendant match. A real engine fixes all four by
construction, and tests/xpath.rs pins each against xmllint's answer.
Two data-model differences between html5ever and libxml2 are reconciled
explicitly rather than left to surprise: a doctype is not an XPath node (§5 lists
seven types and doctype is not among them), and html5ever always synthesizes
<head> and a table <tbody> where libxml2 creates them only when the source
has them. Both are hidden from the data model, and from the serialization, so
//*, //node() and every positional predicate over them agree with the
reference.
CSS. sel hands the selector to scraper. Both gaps this section used to
list are closed and probed. A Selector::parse failure is now a hard error at
build time naming the selector, where it used to map to "no matches" — so a
malformed selector was indistinguishable from one that legitimately matches
nothing, the same silent-answer failure the xpath leg had (5 css_bad probes).
And an attribute value may be written with either quote: arb's command lexer
turns "…" into a string argument, and the reconstruction now re-quotes it, so
a[href="/x"] no longer reaches the parser as a[href=/x].
yq. src/xpath.rs's problem was a missing engine; this leg's was a missing
VALUE MODEL. yq_superset_probe found 61 operators missing, and among them
every node-metadata accessor — anchor, alias, tag, style, the three
comment positions, key, is_key, path, parent, line, column, kind.
Those are not builtins anyone forgot. They read metadata a YAML node carries and
a jq value has no slot for, which is the reason yq exists over jq at all, and
they cannot be added one at a time.
So the metadata rides ALONGSIDE the value, in the shape JqVal::Num(f64, Option<literal>) already used for a number's source text — same problem, same
answer, already in the tree. JqVal::Node boxes a value with its
crate::ynode::NodeMeta, bare() unwraps at every operation whose answer is
about the value, and a node with nothing to record is not boxed at all. The
load-bearing property is that the JSON reader cannot construct the variant: only
crate::yaml can, so a JSON program reaches every answer through exactly the
arms it reached them through before, and the jq leg is untouched by
construction rather than by inspection.
Two things the parser does not hand over are recovered from the SPANS it does. A
# inside a scalar's span is content (pw: "a#b", a comment-looking line inside
a | block) and one outside every span opens a comment; an anchor NAME is read
backwards from its node, over the whitespace and optional tag between them, and
only for a node the parser already reported as anchored. Comment ATTACHMENT
follows six rules measured against yq v4.53.6 rather than taken from its
documentation — the head comment lands on the KEY node, the line comment on the
VALUE node, and a block followed by a blank line becomes the PREVIOUS entry's
foot.
What the probes check, all byte-diffed and none normalized:
| probe | what it asserts |
|---|---|
yq_superset_probe | every enumerated yq operator exists — 61 missing before, 0 now |
yq_probe (111) | each operator's ANSWER equals yq -o=json -I=0's on the same node |
yq_rt_probe (14) | in.yaml; out.yaml returns the SOURCE FILE byte for byte |
yq_norm_probe (3) | a shape yq does not return either: arb normalizes exactly as yq does |
yq_write_probe (8) | a metadata assignment produces the document yq produces |
yq_fmt_probe (12) | out.props is byte-identical to yq -o=props |
The round trip is the strongest of the six, and it is asserted against the source
file rather than against yq's output — which is stricter, not looser. yq '.' is
not idempotent on every shape: it re-folds a > block onto one line, escapes a
non-BMP character as a "\U0001F680" sequence, collapses a multi-line flow
collection, reorders !!map &am to &am !!map, and drops a trailing ....
Requiring arb to match yq there would require arb to reproduce yq's own
infidelities. "The file comes back" is the property the claim names, and it
implies matching yq everywhere yq does return the file.
The three shapes yq will not return either are not dropped for that — a shape
nobody checks is the only bad outcome. yq_norm_probe asserts them against the
REFERENCE instead, byte for byte and nothing normalized, so arb has to normalize
the same way yq does rather than merely being allowed to differ from the source.
Asserting the weaker of two true properties beats asserting neither.
The fixtures are one per feature the model has to carry — comments in every
position (and on sequence items versus mappings), anchors with aliases and merge
keys, deeply nested anchors, aliases inside merge keys, all six scalar styles,
flow versus block, tags on scalars and on collections, empty values and nulls,
non-ASCII, number spellings, explicit ---/... markers, and a multi-document
stream — so a failure names which one broke.
The reference is asked with --yaml-fix-merge-anchor-to-spec. << has two
readings and yq ships both: its default lets a merged key override an explicit
one, and the flag follows the YAML spec. yq's own warning calls the default
"isn't to the yaml spec". arb implements the spec rule, so asking the reference in
its other mode would report a divergence whose content is "arb is right" — which
is not what a divergence should mean. The two modes differ only on merge keys.
out.FORMAT [INDENT] is yq's -o=/-I in arb's spelling (out.yaml,
out.json, out.props, out.xml, out.csv, out.tsv). The DEFAULT rendering
is unchanged: one compact JSON line per document, which is what every existing
pipeline expects.
What the yq leg does not claim. ONE spelling is genuinely out, and it is not
a grammar problem. yq's ref (.a ref $x | $x = 5) binds a MUTABLE HANDLE to a
node, so a later assignment through $x edits the document. Nothing in the value
model is mutable — values are Rc-shared and copied on write, which is what makes
reduce/foreach/path updates affordable — so honouring the spelling would mean
a second, mutable value model, and with(p; f) already provides the capability.
The other two spellings the docs used to list here are ACCEPTED now: .a anchor = "x" and .[] as $item ireduce (0; . + $item) both occupy grammar positions jq
leaves empty, so claiming them cost the jq leg nothing. arb takes yq's spelling
and its own for both.
One behaviour still differs by construction: a node's path/key/parent are
recorded at READ time, so a node relocated by map/pick/+ reports where it
was read from, where yq's real parent pointers would follow it. Closing that means
a reference cycle through Rc for the whole document, which is a worse trade than
the accessor is worth. (filename used to be listed here too; it answers the real
path now when the spec names one with < FILE, and - for a pipe, which is what
yq answers for a piped document.)
Four YAML inputs are answered where yq ERRORS rather than matched — .inf,
-.inf, .nan and an integer past i64 — which is the same direction as the
deviation above: arb answers, the reference refuses. Being more capable than the reference is not a containment failure, and none of the four is "fixed" to match.
What the node model costs. Measured on a 20k-record, 2.3MB document against
the same data as JSON, both on the same debug binary: JSON parses in 0.131s and
the same document as YAML in 1.907s, while adding a filter over the result moves
neither (0.183s and 1.904s). So running a query over boxed values costs the same
as over bare ones — bare() is free in practice — and the metadata is a tax on
the READ, once, not on every operation. Three suspects were measured inside that
read: formatting a throwaway rendering per scalar was a fifth of it and is gone,
and the per-node path vector and the empty-Rc count each moved the total by less
than the run-to-run noise. What remains is saphyr's scanner plus one Rc<YNode>
per YAML node.
What the numbers do not claim. Containment is measured over the surface each
probe ENUMERATES — for xpath that is the 13 axes, 27 core functions and node
tests of the 1999 Recommendation, not XPath 2.0+. Two known boundaries, both
stated rather than papered over: at arb's COMMAND position an expression must
carry an xpath-only character (bogus stays an unknown verb diagnostic rather
than parsing as child::bogus and silently selecting nothing — spell it
//bogus or child::bogus), and contains is the one core-function spelling
jq already answers, so it stays jq's there and XPath's two-argument form is used
inside a predicate, where it is probed.
Every divergence the jq leg had is closed. The last two were:
A SPELLING collision. The native verb table is matched before the jq
fall-through, so arb's native line-per-key verb — spelled keys — shadowed jq's
builtin, and the bare word printed a line per key where jq prints one sorted
array. The native verb is spelled names now and keys in every spelling is
jq's.
A LITERAL loss. A YAML number kept its value but not its source text, so
ratio: 1.50 printed 1.5 where yq prints 1.50 — while the JSON reader
beside it printed 1.50 for the same text. serde's data model has nowhere to
put a number's source text, so the YAML reader composes from the parser's EVENT
stream now (src/yaml.rs) and builds numbers through the same helper the JSON
reader uses. Both are stated in full in
SPEC.md.
The other half of the language — the arithmetic/predicate expressions behind
where, map and calc — has its own harness, scripts/expr_paths.sh. It
diffs three engines per probe: arb on the fusevm interpreter, arb on Cranelift
native code (each pinned with FUSEVM_JIT_BLOCK_THRESHOLD), and jq as the
reference for everything jq can spell. The two arb columns exist because the
default threshold is 1 — the first evaluation of a chunk is interpreted and the
rest are native, so a construct the tiers disagree about answers differently for
a stream's first row than for the rest. A probe only counts against jq when jq
itself exited 0 with output; anything else is reported as skipped, never as a
pass.
Both harnesses refuse to score a run they could not actually measure. The
reference version is pinned, so a different jq on PATH is an error rather
than a quietly different number, and each one counts how many probes really ran
and fails below a floor — otherwise a missing tool drains the whole reference
leg into skipped and the divergence count reaches 0 by comparing nothing.
Where a spelling means one thing to jq and another to arb, context decides: a
bare alphanumeric word is arb's NATIVE verb and the name( CALL spelling is jq's,
so sort_by v is arb's and sort_by(.v) is jq's. Piping into a builtin puts it
in jq context too, so . | flatten is jq's and bare flatten is arb's, and jq
to_entries sits beside native entries.
Context has one boundary, and it is what the keys rename is about: a native
verb spelled EXACTLY like a jq builtin does not just win the bare word, it
SHADOWS jq's, and no context can route both. Every other pair above is safe
because the spellings differ (entries/to_entries, vals/values). keys
was the one that did not, so the native verb took a name of its own — names
for line per key, keys for jq's sorted array. stdlib/json.arb pipes names
into tally and its in-language test pins it. Stated in full in
SPEC.md.
The vocabulary works uniformly over line, JSON (in.json, nested key paths),
CSV/TSV (in.csv/in.tsv), YAML (in.yaml, single- or ----multi-doc), TOML
(in.toml), and HTML streams — one query engine over every format (the yq leg):
in.yaml/in.toml parse the document to JSON so every JSON verb applies. In
families:
- Filter —
match/grep,reject/grepv,contains,starts,ends,nonempty,numeric,over N,under N,between A B,has KEY. - Extract / shape —
field,fields N M…(project/reorder whitespace columns —fields 1 3for columnarps/ls -l/df),pick K…(jq projection),cut,find TAG+attr NAME+text(xpath/css://a/@href),sel {CSS},names(jq'skeysis jq's array),vals,entries,flatten,each,extract /re/,split D,substr A B,chars. - Record edit (jq assignment) —
set K V,del K,rename OLD NEW,default K V,merge. - Transform —
map EXPR,upper/lower/trim/title,replace,prepend/append,pad/lpad,repeat N,flip,words,enumerate,join,floor/ceil,clamp LO HI,delta(consecutive differences — a counter's rate-of-change) /cumsum(running total),sma N(moving average) /ewma A(exponential smoothing — for noisy series feedingspark/chart),commafy,bytes(1536→1.5 KB),duration(3661→1h 1m). - Encode —
b64/b64d,hex/unhex,urlenc/urldec. - Order / dedup —
sort,sort_by F,uniq,unique_by F,dedup,rev,first/last/take/drop/tailn/nth/slice,sample. - Aggregate / reduce —
count,rate,tally,count_by F,sum,min/max,min_by F/max_by F,avg,median,stddev,percentile N(nearest-rank;p50/p90/p95/p99sugar — for latency tails),product,add,range,bins.
The expression layer — where PRED (filter), map EXPR (per-line transform),
calc EXPR (reduce) — lowers to a fusevm::Chunk and runs on the VM, with
field-aware references, compound predicates via and/or/not, and set/range
membership in [a, b, c] / in lo..hi (where ms > 1000 and status in [500, 502, 503], where code in 500..599, map bytes / 1024, where not healthy, map x != 0 ? 100 / x : 0 ternary).
Results render into text/tail/list/gauge/linegauge/bars/histo/
spark/sparkline/scatter/chart/map/calendar/table widgets (table
splits whitespace columns with optional -cols "a,b,c" headers; spark draws a
braille sparkline and sparkline a block-bar one, chart a line plot, scatter
a braille scatter, map a world map of lon lat points, calendar a month grid,
linegauge a thin one-line bar), arranged by grid — grid .w -row R -col C
places a widget, and
-span N (or -rowspan/-colspan) lets one span several cells, so a main
chart can be wide while small gauges take a single cell. Track sizes are
Tk-grid-style: rows "1 2 1" / cols "20c * 2*" give each row/column a fixed
cell count (20), a percentage (30%), or a proportional weight (* = 1, 2*
= 2×); gap N spaces the cells; layout horizontal auto-tiles in a row instead
of a column. Any widget takes
-label "…" to set a human header (instead of the dot-path) and -color NAME
(green/red/yellow/orange/magenta/blue/white/gray, default cyan)
to tint its border and accent — both apply in the TUI and the web dashboard, so
panels read cleanly and can be status-coded (green ok, red errors). list/tail
take -limit N (alias -lines N) to cap the rows shown to the last N.
theme NAME recolors the whole TUI from 31 built-in palettes (the
storageshower HUD schemes shared with the sibling iftoprs/htoprs apps —
neon-noir, blade-runner, night-city, megacorp, … ; arb --list-themes
prints them with swatches), or theme custom c1..c6 for your own 6-index
256-color palette. With a theme active, each widget's default accent is the
theme accent and -color accent|primary|alt|mid|dim|bg resolves through it (a
themeless widget picks a slot by kind, so a dashboard is multi-colored like the
iftop/htop HUD); -color green/red/… stay fixed overrides. A theme is always
on (default neon-sprawl), so the stdlib presets are themed out of the box;
arb --set-theme NAME persists a global default to ~/.arb/config.toml,
--theme NAME overrides per run (find / | arb --fzf --theme neon-noir), and
theme off / --theme off gives the classic cyan look.
Testing pipelines in the language itself
A spec can carry its own unit tests. A test "NAME" { … } block feeds sample
lines through a query pipeline and asserts the output; arb --test spec.arb runs
every block headlessly with TAP output and exits 0
(all passed) / 1 (any failed) — so a dashboard's transforms are regression-tested
in CI, in the same language they're written in.
test "keeps 5xx" {
given "200 ok" "503 down" "404 x" # input lines
run { in; match /5\d\d/ } # any source/out pipeline — jq/xpath too
want "503 down" # expected output
}
$ arb --test dashboard.arb
1..1
ok 1 - keeps 5xx
# 1 passed, 0 failed
[0x05] COMMAND LINE
| Invocation | Effect |
|---|---|
cmd | arb | Zero-config: a full-screen live tail of stdin (type to filter). |
cmd | arb FILE.arb | Run a dashboard spec file. |
cmd | arb -e SRC | Run an inline spec. |
cmd | arb --fzf | fzf select mode: fuzzy-filter + pick line(s) to stdout. |
cmd | arb -- CMD… | Preview pane: re-run CMD over the filtered output. |
arb '<PROD> | _ | <CONS>' | Orchestrate a pipeline; _ is arb's stage, producer stderr → pane. |
arb --run 'PIPELINE' | Same, explicit flag form. |
arb --lsp | Language Server over stdio for .arb (diagnostics, completion, hover, signatureHelp, definition/references/highlight/rename, folding, formatting, semanticTokens). |
arb --dap | Debug Adapter over stdio: step the stream line-by-line, regex breakpoints, function breakpoints on a query verb (where, tally), inspect the paused line / stats / controls. |
arb --check | Validate the spec (parse + build) and exit 0/1, no stdin. |
arb --tiers 'EXPR' | Evaluate EXPR on fusevm, then report which execution tier took its chunk. |
arb --test | Run the spec's in-language test { … } blocks (TAP output), exit 0/1. |
arb -p NAME / --preset | Run a bundled stdlib module by name (-p logs == import logs). -l / --list lists what is available (bundled + ~/.arb/lib). |
arb -r / --repl | Interactive REPL — author and test specs against a sample buffer. |
cmd | arb --json | With an out { … } pipeline, emit results as JSON (array / number / object) instead of plain lines — pipe to jq. |
arb --html | Emit a static HTML dashboard snapshot to stdout and exit. |
arb --dump-tokens / --dump-ast | Print the lexer token stream / parsed command-tree AST and exit. |
arb --dump-bytecode / --disasm | Print the compiled query-pipeline op vectors / a numbered disassembly and exit. |
--version / --help | Version / usage. |
[0x06] ARCHITECTURE
stdin → lexer → parser (AST) → Spec interp → ratatui TUI (or served web + WS)
│
source query pipeline over the live stream
(calc / expressions lower to fusevm bytecode)
Transfers from siblings are mechanics only — fusevm embedding, the rkyv
script cache, the LSP/DAP stdio shape, the package-manager ABI. The language design
(lexer / parser / AST / compiler / semantics) is arb-original. The compute core
already lowers to a fusevm::Chunk and runs on the VM; declarative widget and
layout construction is plain Rust construction and needs no VM.
[0x07] STATUS & ROADMAP
Shipped — the daily-driver path (pipe in → parse spec → query → render, in the terminal or the browser) is complete:
- Language — the Tcl-flavored reader, the declarative widget /
source/outinterpreter,.x <- inbinds,fn/lambda expressions, andcalc/wherepredicates that lower tofusevmbytecode and run on the VM. - Widgets — 25 render kinds:
text,tail,list,gauge,linegauge,bars,histo,spark,sparkline,scatter,chart,map,calendar,table,tabs,block,frame(the full ratatui set — Canvas scatter/ world-map, Monthly calendar, Sparkline, RatatuiLogo, Clear, scrollbar-on- overflow), plus composites:logview(level-colored tail),heatmap,treemap,gantt,diff,logo,clear(spacer),rule(divider);input/filterfields, aslider, achecktoggle, afacetmulti-select,select(an fzf-style fuzzy picker), andsel(an in-dashboard selection list whose highlighted row is published as.<path>.sel) are interactive controls. - Layout — auto-tile (
layout horizontal/vertical) or a proportionalgrid:rows "1 2 1"/cols "20c * 2*"(fixed / percentage / weighted tracks),gap N,-span/-rowspan/-colspanto merge cells. - Themes — an always-on color-theme system of 31 palettes ported from the
sibling
iftoprs/htoprsapps (plustheme custom c1..c6); defaultneon-sprawl, global default in~/.arb/config.toml(arb --set-theme), per-run--theme,arb --list-themes. A themed dashboard recolors from the palette (per-widget slots by kind);-color <slot>/-color greenper widget.Ctrl-Tcycles the theme live in any mode (saved to~/.arb);Ctrl-Gshows a help overlay of the global keys — control keys, since a bare letter is eaten by the filter / text inputs. - Actors —
actor NAME(state) { on MSG(p) { … reply EXPR } }over anmpsc-mailbox thread-per-actor runtime; avia NAME * Npipeline op fans the stream across a supervised pool in parallel, and session refs (spawn/pool/supervise) are driven bytell/askbind/expect actions. - Inline Rust FFI —
rust { pub extern "C" fn … }blocks compile to a cached cdylib (viafusevm) and are callable by name from the expression layer. - Query superset — the
jq/xpath/css/yqverb set in SPEC §8 over JSON, XML, HTML, YAML, TOML, and CSV, measured leg by leg; see Containment, per leg. - Megafilter/map —
out { … }shapes the downstream passthrough, driven byinput/filter/facet/slider/checkcontrols viaapplyand control-path predicates: numericwhere lat < .th, stringwhere match(.q), setwhere level in .lv. - Web target —
arb --servehosts the same spec as a live browser dashboard built from thezgui-corecomponent toolkit (ZGui.appShell+ per-widget components), pushed over a hand-rolled WebSocket (RFC 6455) with a/datapolling fallback;arb --htmlemits a static snapshot. - Reactions & events —
expect /re/ ACTION/bind C-<key> ACTIONwith actionsset/quit/beep/alert/flash/execand{ … }block form; Tk named keys (<Enter>/<Esc>/<Tab>/<Key-x>);timeout Ns ACTIONidle reactions;.w configure -k vretune. - Mouse (SGR, in the TUI) — left-click to focus/toggle a control, drag a
slider, click atabslabel or an fzf row (double-click to pick it); right-click resets a control to its default; middle-click focuses only. The wheel scrolls back through atail/list/table/text/block/frameand returns to the live tail. Shift/Alt/Ctrl modifier bits are decoded too;bind <Click>/bind <Resize>reactions fire on press/resize. Hold Shift and drag for native text selection. - Editor tooling —
arb --lsp, a full Language Server (diagnostics with UTF-16 columns,completion,hover,signatureHelp,definition/references/documentHighlight/renameover widget.pathnames,foldingRange,formatting,semanticTokens), andarb --dap, a real steppable debugger (each stream line is a step, regex breakpoints, function breakpoints on a query verb, the pipeline as the stack,evaluateover the paused line) — both over stdio JSON-RPC. - Presets & library — 150+ bundled stdlib dashboards,
importresolution (withimport X as Ynamespacing), a local preset library (--save/--install/--uninstall/--installed), and a registry over a GitHub-hosted git index (arb update/search/install/add/uninstall, resolved from~/.arb/pkg;arb publish [GIT_URL]upserts the package's entry into the index and pushes it — default registryMenkeTechnologies/arb-registry). - fzf mode —
arb --fzf(rank, smart-case, extended search, multi-select, preview) and pipeline orchestration (arb 'PROD | _ | CONS'). - Self-sourcing specs — a spec can declare its own stream source:
spawn CMD(orspawn { … }) launches a producer whose stdout feeds the stream,< FILEreads a file, and! CMD every Nsre-runs CMD on a timer (headless: once). So a dashboard preset needs nothing piped in (arb top.arbwithspawn top -b). One stream source per spec; a CLI--runproducer wins if both given. - Expect-style automation —
spawn -pty CMDruns the source on a pseudo-terminal (so it acts interactive), and asend "text"action writes to its stdin, soexpect { /password:/ send "hunter2\n" }drives it — scripted interaction with a live process, in the spec. - Zero-config sniffing —
cmd | arb(no spec) peeks the stream and auto-picks a preset by data shape (JSON→json/logs,docker/top/k8sheaders, git-log, CSV→table); a non-blockingpollpeek never hangs, and the peeked lines are replayed so nothing is lost. - rkyv script cache — a re-run of the same spec skips lex+parse: the parsed
AST is cached at
~/.arb/scripts.rkyv(or$ARB_CACHE) as a zero-copy rkyv shard keyed by an FxHash of the source + a schema version, so a source or format change misses cleanly and a corrupt shard resets on its own. Same architecture every sibling lang ships.
Actors ship (SPEC §15) — actor NAME(state) { on MSG(p) { … reply EXPR } }
declares an Akka-style behavior; the runtime is one mpsc-mailbox OS thread per
actor with tell/ask/supervised-pool semantics, and a via NAME * N pipeline op
fans the stream across a worker pool in parallel:
seq 1 1000000 | arb -e 'actor sq(state) { on job(x) { reply x * x } }
out { in; via sq * 8 }' # squared across 8 workers
Two surfaces: the via pipeline op above (parallel stream fan-out), and
session refs driven by events — spawn NAME = ACTOR(init) / pool NAME = ACTOR * N bindings with a supervise NAME { on crash { restart | stop } } crash
policy, driven by tell REF MSG(args) (fire-and-forget) and ask .CTRL REF MSG(args) (reply → a control widget) bind/expect actions in the interactive TUI.
Planned (specified in SPEC.md, not yet built) — native/cdylib
packages and multi-version semver resolution for the registry (the git index,
arb publish, install/search/update all ship), and the upstream-command
sniffing leg (producer argv) — zero-config data-shape sniffing already ships.
Nothing is faked: unrecognized widget verbs are ignored so specs stay forward-compatible, and unbuilt features are absent, not stubbed.
[0x08] DOCUMENTATION
- Read the Docs — the HUD documentation site.
- Reference — every
widget, control, and query builtin, generated from
src/lsp.rs. - Engineering Report — architecture, world-first positioning, milestones, dependency posture.
SPEC.md— the full language spec: grammar, widgets, query, controls, actors, packages.
[0xFF] LICENSE
MIT — free and open source. See LICENSE.