without it the TUI works fully and the web target shows a one-line notice)

September 4, 2026 · View on GitHub

 █████╗ ██████╗ ██████╗
██╔══██╗██╔══██╗██╔══██╗
███████║██████╔╝██████╔╝
██╔══██║██╔══██╗██╔══██╗
██║  ██║██║  ██║██████╔╝
╚═╝  ╚═╝╚═╝  ╚═╝╚═════╝

Rust CI Docs license status

[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

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 shared zgui-core component toolkit.
  • One query engine — a jq/xpath/css/yq superset 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 + orchestratorarb --fzf is 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 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.
  • 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 serve are 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 in src/algo.rs): the same score matrix, boundary/camelCase bonuses, gap penalties and the packed four-slot rank key, so a query returns the same ranking fzf returns, down to the line order. --scheme and --tiebreak reorder 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) in src/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:

    termmatches
    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/--exact a 'term is the FUZZY one. +x / --no-extended turns the whole query back into a single literal fuzzy term; -x / --extended is 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 with rayon and ordered with a parallel sort (src/cli.rs run_filter, src/tui.rs rank), so the ranking runs across every core — 1.26M lines in 123 ms, measured below.

  • Navigate/, Ctrl-J/Ctrl-N down, Ctrl-K/Ctrl-P up.

  • Multi-select — with -m/--multi, Tab marks 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):

queryarbfzfskimvs fzfvs skim
cargo (dense hit rate)123.1 ± 2.0 ms409.3 ± 19.8 ms232.2 ± 11.2 ms3.33×1.89×
libcore96.4 ± 1.0 ms256.9 ± 4.4 ms177.8 ± 4.8 ms2.66×1.84×
'cargo (exact)70.2 ± 0.7 ms200.3 ± 1.5 ms88.0 ± 3.8 ms2.85×1.25×
^/opt .rs$ !zsh49.4 ± 0.8 ms78.2 ± 1.0 ms70.4 ± 2.5 ms1.58×1.42×
zqxjvw (no match)48.8 ± 0.6 ms67.3 ± 0.7 ms61.8 ± 12.4 ms1.38×1.26×
startup, empty stdin4.1 ± 0.4 ms3.2 ± 0.4 ms5.3 ± 0.9 ms0.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.rsinclude_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 — gaugeZGui.gauge, chartZGui.chart, sparkZGui.sparkline, bars/histoZGui.statBars, tableZGui.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

PieceHow
Pipe-nativeTerminal-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 TclCommands take args and verbatim { } blocks; widget paths are dot-hierarchical (.a.b.c). No $, [cmd], or expr{} substitution.
One query engineA 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/mapInteractive 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 fusevmThe 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 / cssarb-native
.users[].namefield users; each; field name
.items[] | select(.price>10)field items; each; where(price>10)
{name, age} (projection)pick name age
//a/@hreffind a; attr href
div.card h2sel {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 2532^{53}, 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:

legreferencecontainmentstatus
jqjq 1.8.2 builtinsevery name presentsuperset, machine-checked
xpathXPath 1.0 (W3C REC) via xmllint48 of 48 enumerated constructs presentsuperset of the enumerated surface
cssSelectors 3/4 via scraper24 selection probes match the equivalent XPathmatches the reference on every probe
yqmikefarah/yq v4.53.6every enumerated operator presentsuperset 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:

probewhat it asserts
yq_superset_probeevery 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

2532^{53} 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:

  • Filtermatch/grep, reject/grepv, contains, starts, ends, nonempty, numeric, over N, under N, between A B, has KEY.
  • Extract / shapefield, fields N M… (project/reorder whitespace columns — fields 1 3 for columnar ps/ls -l/df), pick K… (jq projection), cut, find TAG + attr NAME + text (xpath/css: //a/@href), sel {CSS}, names (jq's keys is 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.
  • Transformmap 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 feeding spark/chart), commafy, bytes (15361.5 KB), duration (36611h 1m).
  • Encodeb64/b64d, hex/unhex, urlenc/urldec.
  • Order / dedupsort, sort_by F, uniq, unique_by F, dedup, rev, first/last/take/drop/tailn/nth/slice, sample.
  • Aggregate / reducecount, rate, tally, count_by F, sum, min/ max, min_by F/max_by F, avg, median, stddev, percentile N (nearest-rank; p50/p90/p95/p99 sugar — 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 gridgrid .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

InvocationEffect
cmd | arbZero-config: a full-screen live tail of stdin (type to filter).
cmd | arb FILE.arbRun a dashboard spec file.
cmd | arb -e SRCRun an inline spec.
cmd | arb --fzffzf 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 --lspLanguage Server over stdio for .arb (diagnostics, completion, hover, signatureHelp, definition/references/highlight/rename, folding, formatting, semanticTokens).
arb --dapDebug 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 --checkValidate 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 --testRun the spec's in-language test { … } blocks (TAP output), exit 0/1.
arb -p NAME / --presetRun a bundled stdlib module by name (-p logs == import logs). -l / --list lists what is available (bundled + ~/.arb/lib).
arb -r / --replInteractive REPL — author and test specs against a sample buffer.
cmd | arb --jsonWith an out { … } pipeline, emit results as JSON (array / number / object) instead of plain lines — pipe to jq.
arb --htmlEmit a static HTML dashboard snapshot to stdout and exit.
arb --dump-tokens / --dump-astPrint the lexer token stream / parsed command-tree AST and exit.
arb --dump-bytecode / --disasmPrint the compiled query-pipeline op vectors / a numbered disassembly and exit.
--version / --helpVersion / 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 / out interpreter, .x <- in binds, fn/lambda expressions, and calc / where predicates that lower to fusevm bytecode 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/filter fields, a slider, a check toggle, a facet multi-select, select (an fzf-style fuzzy picker), and sel (an in-dashboard selection list whose highlighted row is published as .<path>.sel) are interactive controls.
  • Layout — auto-tile (layout horizontal/vertical) or a proportional grid: rows "1 2 1" / cols "20c * 2*" (fixed / percentage / weighted tracks), gap N, -span/-rowspan/-colspan to merge cells.
  • Themes — an always-on color-theme system of 31 palettes ported from the sibling iftoprs/htoprs apps (plus theme custom c1..c6); default neon-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 green per widget. Ctrl-T cycles the theme live in any mode (saved to ~/.arb); Ctrl-G shows a help overlay of the global keys — control keys, since a bare letter is eaten by the filter / text inputs.
  • Actorsactor NAME(state) { on MSG(p) { … reply EXPR } } over an mpsc-mailbox thread-per-actor runtime; a via NAME * N pipeline op fans the stream across a supervised pool in parallel, and session refs (spawn/pool/supervise) are driven by tell/ask bind/expect actions.
  • Inline Rust FFIrust { pub extern "C" fn … } blocks compile to a cached cdylib (via fusevm) and are callable by name from the expression layer.
  • Query superset — the jq/xpath/css/yq verb set in SPEC §8 over JSON, XML, HTML, YAML, TOML, and CSV, measured leg by leg; see Containment, per leg.
  • Megafilter/mapout { … } shapes the downstream passthrough, driven by input/filter/facet/slider/check controls via apply and control-path predicates: numeric where lat < .th, string where match(.q), set where level in .lv.
  • Web targetarb --serve hosts the same spec as a live browser dashboard built from the zgui-core component toolkit (ZGui.appShell + per-widget components), pushed over a hand-rolled WebSocket (RFC 6455) with a /data polling fallback; arb --html emits a static snapshot.
  • Reactions & eventsexpect /re/ ACTION / bind C-<key> ACTION with actions set/quit/beep/alert/flash/exec and { … } block form; Tk named keys (<Enter>/<Esc>/<Tab>/<Key-x>); timeout Ns ACTION idle reactions; .w configure -k v retune.
  • Mouse (SGR, in the TUI) — left-click to focus/toggle a control, drag a slider, click a tabs label 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 a tail/list/table/text/block/frame and 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 toolingarb --lsp, a full Language Server (diagnostics with UTF-16 columns, completion, hover, signatureHelp, definition/ references/documentHighlight/rename over widget .path names, foldingRange, formatting, semanticTokens), and arb --dap, a real steppable debugger (each stream line is a step, regex breakpoints, function breakpoints on a query verb, the pipeline as the stack, evaluate over the paused line) — both over stdio JSON-RPC.
  • Presets & library — 150+ bundled stdlib dashboards, import resolution (with import X as Y namespacing), 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 registry MenkeTechnologies/arb-registry).
  • fzf modearb --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 (or spawn { … }) launches a producer whose stdout feeds the stream, < FILE reads a file, and ! CMD every Ns re-runs CMD on a timer (headless: once). So a dashboard preset needs nothing piped in (arb top.arb with spawn top -b). One stream source per spec; a CLI --run producer wins if both given.
  • Expect-style automationspawn -pty CMD runs the source on a pseudo-terminal (so it acts interactive), and a send "text" action writes to its stdin, so expect { /password:/ send "hunter2\n" } drives it — scripted interaction with a live process, in the spec.
  • Zero-config sniffingcmd | arb (no spec) peeks the stream and auto-picks a preset by data shape (JSON→json/logs, docker/top/k8s headers, git-log, CSV→table); a non-blocking poll peek 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.